目录
一. jwt的概念
1. Session的工作流程
当用户访问服务器时,服务端返回一个随机字符串(sessionID)
当用户登录成功后,把随机字符串(sessionID)组织成键值对,加到cookie中发送给用户
用户再次发送请求时,将响应头中的随机字符串(sessionID)添加到请求头中发送到后台
后台进行验证,是否存在用户信息,存在进行放行,不存在拦截掉
2. 在前后端分离中不使用session的原因
session储存在后端服务器中,浏览器访问的是前端服务器,在浏览输入网址时不能获取到session
3. jwt的工作流程
用户发送登录请求,后端服务器返回一个jwt令牌
前端服务器将后端服务器返回的jwt令牌进行保存
用户再次发送请求时,在请求头中携带jwt令牌到后端服务器
后台进行验证,是否存在用户信息,存在进行放行,不存在拦截掉
4. jwt令牌组成
二. jwt的工具类介绍
1. JwtUtils
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();
}
}
2. JwtFilter
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. 测试
```java
@Test
public void test1() {// 生成JWT
//JWT Token=Header.Payload.Signature
//头部.载荷.签名
//Payload=标准声明+私有声明+公有声明
//定义私有声明
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("username", "zss");
claims.put("age", 18);
//TTL:Time To Live
String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL);
System.out.println(jwt);
//获取Payload(包含标准和私有声明)
Claims parseJwt = JwtUtils.parseJwt(jwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
}
```
@Test
public void test2() {// 解析oldJwt
//io.jsonwebtoken.ExpiredJwtException:JWT过期异常
//io.jsonwebtoken.SignatureException:签名异常
//String oldJwt="eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTA3MTg2NzcsImlhdCI6MTU5MDcxNjg3NywiYWdlIjoxOCwianRpIjoiNDFmZjFiZGFkYzkxNDA3OGE4ZGUyNGRkZDEwYjU4N2IiLCJ1c2VybmFtZSI6InpzcyJ9.DdPvioX6kuhV6lEfD9QAN2eQSk_mO3dYkmDmTQsqa78";
//eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzU1NjE3MjcsImlhdCI6MTYzNTU1OTkyNywiYWdlIjoxOCwianRpIjoiN2RlYmIzM2JiZTg3NDBmODgzNDI5Njk0ZWE4NzcyMTgiLCJ1c2VybmFtZSI6InpzcyJ9.dUR-9JUlyRdoYx-506SxXQ3gbHFCv0g5Zm8ZGzK1fzw
String newJwt="eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6a2luZyIsImV4cCI6MTY5MDkwNzU2MiwiaWF0IjoxNjkwOTA1NzYyLCJhZ2UiOjE4LCJqdGkiOiIyNmE2NzE5N2YzNjI0MWU4ODQyY2FkNjA1Yjg5Y2RmNyIsInVzZXJuYW1lIjoienNzIn0.d69yQi3ZVEbDMcmRpcv3VDY8U2KYxAbgbusY05DCPLA";
String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzU1NjE3MjcsImlhdCI6MTYzNTU1OTkyNywiYWdlIjoxOCwianRpIjoiN2RlYmIzM2JiZTg3NDBmODgzNDI5Njk0ZWE4NzcyMTgiLCJ1c2VybmFtZSI6InpzcyJ9.dUR-9JUlyRdoYx-506SxXQ3gbHFCv0g5Zm8ZGzK1fzw";
Claims parseJwt = JwtUtils.parseJwt(newJwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
}
oldJwt
@Test
public void test3() {// 复制jwt,并延时30分钟
String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6a2luZyIsImV4cCI6MTY5MDkwNzU2MiwiaWF0IjoxNjkwOTA1NzYyLCJhZ2UiOjE4LCJqdGkiOiIyNmE2NzE5N2YzNjI0MWU4ODQyY2FkNjA1Yjg5Y2RmNyIsInVzZXJuYW1lIjoienNzIn0.d69yQi3ZVEbDMcmRpcv3VDY8U2KYxAbgbusY05DCPLA";
//String newJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDU3NTM2NTUsImlhdCI6MTYwNTc1MTg1NSwiYWdlIjoxOCwianRpIjoiYmNmN2Q1MzQ2YjE3NGU2MDk1MmIxYzQ3ZTlmMzQyZjgiLCJ1c2VybmFtZSI6InpzcyJ9.m1Qn84RxgbKCnsvrdbbAnj8l_5Jwovry8En0j4kCxhc";
//String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjI5MDMzNjAsImlhdCI6MTU2MjkwMTU2MCwiYWdlIjoxOCwianRpIjoiZDVjMzE4Njg0MDcyNDgyZDg1MDE5ODVmMDY3OGQ4NjkiLCJ1c2VybmFtZSI6InpzcyJ9.XDDDRRq5jYq5EdEBHtPm7GcuBz4S0VhDTS1amRCdf48";
String newJwt = JwtUtils.copyJwt(oldJwt, JwtUtils.JWT_WEB_TTL);
System.out.println(newJwt);
Claims parseJwt = JwtUtils.parseJwt(newJwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
}
三. jwt集成到spa项目
1. 后台
user登录方法,要放开用户信息生成的jwt串保存到响应头中的代码
关闭jwtfilter中的off开关 ,代表开启jwt验证
crosfilter中要允许jwt使用请求头及相应头
2. 前台
1在state.js中加入变量jwt
2在mutations.js中添加setJwt方法
setJwt: function(state, payload) {
state.jwt = payload.jwt;
}
3在getters.js中添加getJwt方法
getJwt: function(state) {
return state.jwt;
}
4将vue实例赋给一个变量
window.vm = new Vue({
el: '#app',
data() {
return {
BUS: new Vue({})
}
},
router,
store,
components: {
App
},
template: '<App/>'
})
5 设置前台服务器的响应拦截器
axios.interceptors.response.use(function(response) {
let jwt = response.headers['jwt'];
if (jwt) {
window.vm.$store.commit('setJwt', {
jwt: jwt
})
}
window.vm.$store.commit('setJwt', {
jwt: jwt
})
return response;
}, function(error) {
return Promise.reject(error);
});
6设置前台服务器的请求拦截器
axios.interceptors.request.use(function(config) {
let jwt = window.vm.$store.getters.getJwt;
config.headers['jwt'] = jwt;
return config;
}, function(error) {
return Promise.reject(error);
});
3. 效果
正常登录流程
直接访问