一. jwt的简介
JWT全称为JSON Web Token,是一种用于身份验证和授权的开放标准。它是一个紧凑且安全的方式,通过在不同系统之间传递可信任的信息来实现用户身份验证。JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
头部包含了描述令牌类型和所使用的加密算法的元数据。常见的加密算法有HMAC、RSA等。载荷包含了要传输的数据,比如用户ID、角色和其他相关的声明信息。载荷可以自定义,但需要遵循一定的规范。
签名是对头部和载荷进行签名生成的,用于验证令牌的真实性和完整性。它使用头部中指定的加密算法和密钥进行签名,并将签名与头部和载荷一起编码为一个字符串形式的JWT。
JWT的精髓在于:“去中心化”,数据是保存在客户端的。
JWT的工作流程通常如下:当用户登录时,服务器会生成一个JWT并返回给客户端。客户端在后续的请求中将这个JWT放在请求的头部或参数中发送给服务器。服务器在接收到请求后会解析JWT,并根据其中的信息进行身份验证和授权。
JWT的优点是可以避免在服务器端存储session信息,使得服务端无状态化,提高了系统的可扩展性。同时,JWT还具有跨域支持和自包含性的特点,使得它在分布式系统中广泛应用。
需要注意的是,JWT仅仅对于信息的完整性和真实性提供了保护,但并不加密具体的数据。因此,在使用JWT时,应避免在载荷中包含敏感信息,或者对敏感信息进行加密处理。另外,由于JWT是基于Token的认证方式,为了确保安全性,应采取适当的措施保护JWT的传输过程和存储过程。
二. jwt工具类介绍
JWT工具类是一个用于生成、解析和验证JSON Web Token的辅助类。它提供了一系列方法来简化JWT的处理过程,使得开发者可以方便地在应用中使用JWT进行身份验证和授权。
JWT工具类通常包括以下功能:
- 生成JWT:提供方法用于生成JWT,传入有效载荷和加密密钥等参数,生成符合规范的JWT字符串。
- 解析JWT:提供方法用于解析JWT,将JWT字符串解析成头部和载荷的JSON对象,并对签名进行验证。
- 验证JWT:提供方法用于验证JWT的有效性,包括验证签名是否正确、验证令牌是否过期等。
- 获取载荷信息:提供方法用于获取JWT中的载荷信息,比如用户ID、角色等。
- 刷新JWT:提供方法用于刷新JWT,生成新的JWT并返回给客户端,用于延长认证的有效期。
- 配置参数:提供方法用于配置JWT的相关参数,比如加密算法、密钥、过期时间等。
使用JWT工具类可以简化JWT的处理逻辑,减少代码重复性,并增加系统的安全性。通过封装好的方法,开发者可以方便地在不同的场景中使用JWT进行身份验证和授权,提高开发效率。但需要注意,在使用JWT工具类时,应确保密钥的安全性,避免泄露导致令牌被篡改或伪造。
package com.zking.ssm.jwt; import java.util.Date; import java.util.Map; import java.util.UUID; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; /** * JWT验证过滤器:配置顺序 CorsFilte->JwtUtilsr-->StrutsPrepareAndExecuteFilter * */ public class JwtUtils { /** * JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认30分钟 */ public static final long JWT_WEB_TTL = 30 * 60 * 1000; /** * 将jwt令牌保存到header中的key */ public static final String JWT_HEADER_KEY = "jwt"; // 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。 private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙 private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key static { byte[] encodedKey = Base64.decodeBase64(JWT_SECRET); JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); } private JwtUtils() { } /** * 解密jwt,获得所有声明(包括标准和私有声明) * * @param jwt * @return * @throws Exception */ public static Claims parseJwt(String jwt) { Claims claims = Jwts.parser() .setSigningKey(JWT_KEY) .parseClaimsJws(jwt) .getBody(); return claims; } /** * 创建JWT令牌,签发时间为当前时间 * * @param claims * 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) * @param ttlMillis * JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间 * @return jwt令牌 */ public static String createJwt(Map<String, Object> claims, long ttlMillis) { // 生成JWT的时间,即签发时间 2021-10-30 10:02:00 -> 30 10:32:00 long nowMillis = System.currentTimeMillis(); //链式语法: // 下面就是在为payload添加各种标准声明和私有声明了 // 这里其实就是new一个JwtBuilder,设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。 // 可以在未登陆前作为身份标识使用 .setId(UUID.randomUUID().toString().replace("-", "")) // iss(Issuser)签发者,写死 .setIssuer("zking") // iat: jwt的签发时间 .setIssuedAt(new Date(nowMillis)) // 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放 // .setSubject("{}") // 设置签名使用的签名算法和签名使用的秘钥 .signWith(SIGNATURE_ALGORITHM, JWT_KEY) // 设置JWT的过期时间 .setExpiration(new Date(nowMillis + ttlMillis)); return builder.compact(); } /** * 复制jwt,并重新设置签发时间(为当前时间)和失效时间 * * @param jwt * 被复制的jwt令牌 * @param ttlMillis * jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间 * @return */ public static String copyJwt(String jwt, Long ttlMillis) { //解密JWT,获取所有的声明(私有和标准) //old Claims claims = parseJwt(jwt); // 生成JWT的时间,即签发时间 long nowMillis = System.currentTimeMillis(); // 下面就是在为payload添加各种标准声明和私有声明了 // 这里其实就是new一个JwtBuilder,设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。 // 可以在未登陆前作为身份标识使用 //.setId(UUID.randomUUID().toString().replace("-", "")) // iss(Issuser)签发者,写死 // .setIssuer("zking") // iat: jwt的签发时间 .setIssuedAt(new Date(nowMillis)) // 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放 // .setSubject("{}") // 设置签名使用的签名算法和签名使用的秘钥 .signWith(SIGNATURE_ALGORITHM, JWT_KEY) // 设置JWT的过期时间 .setExpiration(new Date(nowMillis + ttlMillis)); return builder.compact(); } }
三. jwt集成spa项目
后台要求:
1. 用户登录方法,放开用户信息生成jwt串保存到响应头中
package com.zking.ssm.controller; import com.zking.ssm.service.IUserService; import com.zking.ssm.util.JsonResponseBody; import com.zking.ssm.util.PageBean; import com.zking.ssm.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.List; import java.util.Map; import com.zking.ssm.jwt.*; @Controller @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @RequestMapping("/userLogin") @ResponseBody public JsonResponseBody<?> userLogin(UserVo userVo, HttpServletResponse response){ if(userVo.getUsername().equals("admin")&&userVo.getPassword().equals("123")){ //私有要求claim Map<String,Object> json=new HashMap<String,Object>(); json.put("username", userVo.getUsername()); // 生成JWT,并设置到response响应头中 String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL); response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt); return new JsonResponseBody<>("用户登陆成功!",true,0,null); }else{ return new JsonResponseBody<>("用户名或密码错误!",false,0,null); } } @RequestMapping("/userRegister") @ResponseBody public JsonResponseBody<?> userRegistered(UserVo userVo, HttpServletRequest request){ int insertSelective = userService.insertSelective(userVo); if(insertSelective>0){ return new JsonResponseBody<>("用户注册成功!",true,0,null); }else{ return new JsonResponseBody<>("注册失败,请稍后!",false,0,null); } } @RequestMapping("/queryUserPager") @ResponseBody public JsonResponseBody<List<Map<String,Object>>> queryUserPager(UserVo userVo, HttpServletRequest request){ try { PageBean pageBean=new PageBean(); pageBean.setRequest(request); List<Map<String, Object>> users = userService.queryUserPager(userVo, pageBean); return new JsonResponseBody<>("OK",true,pageBean.getTotal(),users); } catch (Exception e) { e.printStackTrace(); return new JsonResponseBody<>("分页查询用户信息失败!",false,0,null); } } }
2. 关闭JwtFilter中的OFF开关,开启jwt验证
package com.zking.ssm.jwt; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import io.jsonwebtoken.Claims; /** * * JWT验证过滤器,配置顺序 :CorsFilter-->JwtFilter-->struts2中央控制器 * * @author Administrator * */ public class JwtFilter implements Filter { // 排除的URL,一般为登陆的URL(请改成自己登陆的URL) private static String EXCLUDE = "^/user/userLogin?.*$"; private static Pattern PATTERN = Pattern.compile(EXCLUDE); private boolean OFF = false;// true关闭jwt令牌验证功能 @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; //获取当前请求路径。只有登录的请求路径不进行校验之外,其他的URL请求路径必须进行JWT令牌校验 //http://localhost:8080/ssh2/bookAction_queryBookPager.action //req.getServletPath()==/bookAction_queryBookPager.action String path = req.getServletPath(); if (OFF || isExcludeUrl(path)) {// 登陆直接放行 chain.doFilter(request, response); return; } // 从客户端请求头中获得令牌并验证 //token=头.载荷.签名 String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY); Claims claims = this.validateJwtToken(jwt); //在这里请各位大哥大姐从JWT令牌中提取payload中的声明部分 //从声明部分中获取私有声明 //获取私有声明中的User对象 -> Modules Boolean flag=false; if (null == claims) { // resp.setCharacterEncoding("UTF-8"); resp.sendError(403, "JWT令牌已过期或已失效"); return; } else { //1.获取已经解析后的payload(私有声明) //2.从私有声明中当前用户所对应的权限集合List<String>或者List<Module> //3.循环权限(Module[id,url]) // OK,放行请求 chain.doFilter(request, response); // NO,发送错误信息的JSON // ObjectMapper mapper=new ObjectMapper() // mapper.writeValue(response.getOutputStream(),json) String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL); resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt); chain.doFilter(request, response); } } /** * 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败 */ private Claims validateJwtToken(String jwt) { Claims claims = null; try { if (null != jwt) { //该解析方法会验证:1)是否过期 2)签名是否成功 claims = JwtUtils.parseJwt(jwt); } } catch (Exception e) { e.printStackTrace(); } return claims; } /** * 是否为排除的URL * * @param path * @return */ private boolean isExcludeUrl(String path) { Matcher matcher = PATTERN.matcher(path); return matcher.matches(); } // public static void main(String[] args) { // String path = "/sys/userAction_doLogin.action?username=zs&password=123"; // Matcher matcher = PATTERN.matcher(path); // boolean b = matcher.matches(); // System.out.println(b); // } }
3. web.xml中要配置corsfilter,允许jwt使用请求头及响应头
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>Archetype Created Web Application</display-name> <!--1.实现Spring与Web集成,实现Spring上下文的初始化工作--> <!-- spring上下文配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </context-param> <!-- 读取Spring上下文的监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--2.配置中文乱码过滤器,使用Spring自带的过滤器--> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <async-supported>true</async-supported> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--CrosFilter跨域过滤器--> <filter> <filter-name>corsFilter</filter-name> <filter-class>com.zking.ssm.util.CorsFilter</filter-class> </filter> <filter-mapping> <filter-name>corsFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--JwtFilter--> <filter> <filter-name>jwtFilter</filter-name> <filter-class>com.zking.ssm.jwt.JwtFilter</filter-class> </filter> <filter-mapping> <filter-name>jwtFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Spring MVC核心控制器--> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--此参数可以不配置,默认值为:/WEB-INF/springmvc-servlet.xml--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <!--web.xml 3.0的新特性,是否支持异步--> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
前端代码:
state.js
export default{ wxName:'派大星', jwt:'' }
mutations.js
export default{ // state指的是state.js文件中导出的对象 // payload就是vue文件传递过来的参数 setWxName:(state,payload)=>{ state.wxName = payload.wxName }, setJwt:(state,payload)=>{ state.jwt = payload.jwt } }
getters.js
export default{ getWxName:(state)=>{ return state.wxName; }, getJwt:(state)=>{ return state.jwt; } }
http.js
/** * vue项目对axios的全局配置 */ import axios from 'axios' import qs from 'qs' //引入action模块,并添加至axios的类属性urls上 import action from '@/api/action' axios.urls = action // axios默认配置 axios.defaults.timeout = 10000; // 超时时间 // axios.defaults.baseURL = 'http://localhost:8080/j2ee15'; // 默认地址 axios.defaults.baseURL = action.SERVER; //整理数据 // 只适用于 POST,PUT,PATCH,transformRequest` 允许在向服务器发送前,修改请求数据 axios.defaults.transformRequest = function(data) { data = qs.stringify(data); return data; }; // 请求拦截器 axios.interceptors.request.use(function(config) { let jwt = window.vm.$store.getters.getJwt; if(jwt){ config.headers['jwt']=jwt; } return config; }, function(error) { return Promise.reject(error); }); // 响应拦截器 axios.interceptors.response.use(function(response) { let jwt = response.headers['jwt']; if(jwt){ // 要将响应头中的jwt串放入到state.js中 window.vm.$store.commit('setJwt',{ jwt:jwt }); } return response; }, function(error) { return Promise.reject(error); }); export default axios;
main.js
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' //开发环境下才会引入mockjs // process.env.MOCK && require('@/mock') // 新添加1 import ElementUI from 'element-ui' // 新添加2,避免后期打包样式不同,要放在import App from './App';之前 import 'element-ui/lib/theme-chalk/index.css' import App from './App' import router from './router' import store from './store' // 新添加3 Vue.use(ElementUI) Vue.config.productionTip = false import axios from '@/api/http' import VueAxios from 'vue-axios' Vue.use(VueAxios,axios) /* eslint-disable no-new */ window.vm = new Vue({ el: '#app', router, store, data(){ return { Bus:new Vue() } }, components: { App }, template: '<App/>' })