違背的青春
- Spring Security(一):整合JWT實作登入功能
- Spring Security(二):擷取使用者權限菜單樹
- Spring Security(三):與Vue.js整合
- Spring Security(四):更新前端路由擷取方式
- Spring Security(五):前後端權限控制詳解
- Spring Security(六):前端菜單,角色權限頁面的搭建
- Spring Security(七):自定義攔截器實作對權限異常的處理
- Spring Security(八):Vue.js使用 CryptoJS加密密碼以及BCryptPasswordEncoder的使用
當使用
jwt
進行校驗使用者資訊,由于
jwt
是無狀态的,是以最重要的就是解決重新整理
token
帶來問題,一個簡單的需求就是15天内免登入。當然最簡單粗暴的方式是你可以設
token
有效期為半個月嘛,這确實是一個解決方案,但是對于資訊比較重要的還是不要采用這種方案,最好把
token
的有效期時間設定為短一些使用
refresh
重新整理
token
就ok。
大概流程如下圖
簡單解釋下:現在我采取的方案是設定
token
的有效期為兩小時,大概流程
- 使用者通過登入,後端會存儲這個
到token
,key設定為Redis
,value設定為userid
,有效期設定為15天(如果一周免登陸,那token
的有效期就要設定為1周)Redis
- 如果使用者在兩個小時後請求資料,這個
是失效的,但是我們可以通過這個失效的token
擷取token
中的使用者資訊(我把使用者id存儲到了payload
的token
中),通過解析payload
擷取使用者id,根據這個id去查到token
這個key值是否存在,如果還在Redis
有效期内我們就可以重新簽發Redis
傳回給前端,前端把之前存儲的token
替換為現在得,周而複始,token
- 如果時間過了規定的免登入時間那
中,Redis
token
會失效,我們隻需要傳回失效資訊給前端處理就完事了。
關鍵代碼:
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Resource
private RedisUtil redisUtil;
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {
String authHeader = request.getHeader(jwtTokenUtil.getHeader());
try {
if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
String username = jwtTokenUtil.getUsernameFromToken(authHeader);
jwtTokenUtil.validateToken(authHeader);//驗證令牌
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authHeader)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
catch (ExpiredJwtException e){
e.printStackTrace();
Map<String,Object> map = jwtTokenUtil.parseJwtPayload(authHeader);
String userid = (String)map.get("userid");
//這裡的方案是如果令牌過期了,先去判斷redis中存儲的令牌是否過期,如果過期就重新登入,如果redis中存儲的沒有過期就可以
//繼續生成token傳回給前端存儲方式key:userid,value:令牌
String redisResult = redisUtil.get(userid);
String username= (String) map.get("sub");
if(StringUtils.isNoneEmpty(redisResult)){
JwtUser jwtUser = new JwtUser();
jwtUser.setUserid(userid);
jwtUser.setUsername(username);
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", jwtUser.getUsername());
claims.put("userid", jwtUser.getUserid());
claims.put("created", new Date());
String token = jwtTokenUtil.generateToken(jwtUser);
//更新redis中的token
//首先擷取key的有效期,把新的token的有效期設為舊的token剩餘的有效期
redisUtil.setAndTime(userid,token,redisUtil.getExpireTime(userid));
if (token != null && StringUtils.isNotEmpty(token)) {
jwtTokenUtil.validateToken(token);//驗證令牌
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
response.setHeader("newToken",token);
response.addHeader("Access-Control-Expose-Headers","newToken");
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
try {
chain.doFilter(request, response);
} catch (IOException e1) {
e1.printStackTrace();
} catch (ServletException e1) {
e1.printStackTrace();
}
} else {
response.addHeader("Access-Control-Allow-origin","http://localhost:9528");
RetResult retResult = new RetResult(RetCode.EXPIRED.getCode(),"抱歉,您的登入資訊已過期,請重新登入");
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(retResult));
System.out.println("redis過期");
}
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
主要修改了
JwtAuthenticationTokenFilter
的邏輯代碼,其實很簡單就是首先判斷
jwt
是否過期,然後再判斷
Redis
是否過期,根絕結果傳回資訊罷了。
幾個問題
1.重新整理
token
如何傳回前端,這裡隻介紹
Spring Boot
的傳回方式,很簡單就是通過
response
傳回
token
給前端,注意一點就是需要添加一個自定義頭資訊我設為
newToken
,這樣前端才能擷取到,如果不添加
response.addHeader("Access-Control-Expose-Headers","newToken");
這段代碼,前端是擷取不到我們新簽發的
token
哦
response.setHeader("newToken",token);
response.addHeader("Access-Control-Expose-Headers","newToken");
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
效果大概如下:
而且以後的請求,都是使用了這個重新整理的
token
通路的
2.
token
改變了之後那我們後續的請求肯定是不行了,如何解決?
很好解決,拿着這個token,繼續走
JwtAuthenticationTokenFilter
中的驗權代碼即可
redisUtil.setAndTime(userid,token,redisUtil.getExpireTime(userid));
if (token != null && StringUtils.isNotEmpty(token)) {
jwtTokenUtil.validateToken(token);//驗證令牌
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
3.前端如何擷取新簽發的
token
?
隻需要在
srcutilsrequest.js
中修改相關代碼,判斷我們是上面傳回的
newToken
,如果這個
response
作用域中有該字段,就證明後端更新token,我們前端需要修改
token
了,然後調用
setToken
即可,後續的請求中就會使用了該
token
去請求接口。
if(response.headers.newtoken){
setToken(response.headers.newtoken)
}
4.如何以一定的格式傳回給前端錯誤資訊,這裡提供一個例子,第一行的代碼是為了解決跨域問題,然後傳回一個
Json
類型的字元串就好。
response.addHeader("Access-Control-Allow-origin","http://localhost:9528");
RetResult retResult = new RetResult(RetCode.EXPIRED.getCode(),"抱歉,您的登入資訊已過期,請重新登入");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(retResult));
5.如何解析過期的
token
?由于
jwt
的
payload
就是
key-value
格式,是以這裡我傳回了一個
map
格式
private static final ObjectMapper MAPPER = new ObjectMapper();
private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver();
public static Map parseJwtPayload(String jwt) {
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null;
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
for (char c : jwt.toCharArray()) {
if (c == '.') {
CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb);
String token = tokenSeq != null ? tokenSeq.toString() : null;
if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
if (delimiterCount != 2) {
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
throw new MalformedJwtException(msg);
}
if (sb.length() > 0) {
base64UrlEncodedDigest = sb.toString();
}
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
CompressionCodec compressionCodec = null;
if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map<String, Object> m = readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}
compressionCodec = codecResolver.resolveCompressionCodec(header);
}
// =============== Body =================
String payload;
if (compressionCodec != null) {
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8);
} else {
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
}
JSONObject jsonObject = JSONObject.parseObject(payload);
Map<String, Object> map = jsonObject;
return map;
}
/**
* @Description
* @Param [val] 從json資料中讀取格式化map
* @Return java.util.Map<java.lang.String,java.lang.Object>
*/
public static Map<String, Object> readValue(String val) {
try {
return MAPPER.readValue(val, Map.class);
} catch (IOException e) {
throw new MalformedJwtException("Unable to read JSON value: " + val, e);
}
}
歡迎關注我的個人公衆号~github:
ywbjja/springSecurity-jwt-vue-templegithub.com
個人微信公衆号~