功能需求
????????最近項(xiàng)目中有這么一個(gè)功能,用戶登錄系統(tǒng)后,需要給 用戶 頒發(fā)一個(gè) token ,后續(xù)訪問(wèn)系統(tǒng)的請(qǐng)求都需要帶上這個(gè) token ,如果請(qǐng)求沒(méi)有帶上這個(gè) token 或者 token 過(guò)期了,那么禁止訪問(wèn)系統(tǒng)。如果用戶一直訪問(wèn)系統(tǒng),那么還需要自動(dòng)延長(zhǎng) token 的過(guò)期時(shí)間。
注意
不推薦使用,存在 refreshToken 的泄漏風(fēng)險(xiǎn),應(yīng)該在 token 過(guò)期時(shí),再次發(fā)送請(qǐng)求使用 refreshToken 刷新 token。
功能分析
1、token 的生成
使用現(xiàn)在比較流行的 jwt 來(lái)生成。
2、token 的自動(dòng)延長(zhǎng)
要實(shí)現(xiàn) token 的自動(dòng)延長(zhǎng),系統(tǒng)給用戶 頒發(fā) 一個(gè) token 無(wú)法實(shí)現(xiàn),那么通過(guò)變通一個(gè),給用戶生成 2個(gè) token ,一個(gè)用于 api 訪問(wèn)的 token ,一個(gè) 用于在 token 過(guò)期的時(shí)候 用來(lái) 刷新 的 refreshToken。并且 refreshToken 的 生命周期要比 token 的生命周期長(zhǎng)。
3、系統(tǒng)資源的保護(hù)
可以使用Spring Security 來(lái)保護(hù)系統(tǒng)的各種資源。
4、用戶如何傳遞 token
系統(tǒng)中 token 和 refreshToken 的傳遞一律放在請(qǐng)求頭。
實(shí)現(xiàn)思路
1、生成 token 和 refreshToken
用戶登錄系統(tǒng)的時(shí)候,后臺(tái)給用戶生成 token 和 refreshToken 并放在響應(yīng)頭中返回
2、系統(tǒng) 判斷 token 是否合法
token未失效的時(shí)的處理token失效 ,如何使用refreshToken來(lái)生成新的token

核心代碼如下
1、過(guò)濾器代碼,token判斷和再次生成
package com.huan.study.security.token;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huan.study.security.configuration.TokenProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @author huan 2020-06-07 - 14:34
*/
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class TokenAuthenticateFilter extends OncePerRequestFilter {
private final TokenProperties tokenProperties;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 獲取 認(rèn)證頭
String authorizationHeader = request.getHeader(tokenProperties.getAuthorizationHeaderName());
if (!checkIsTokenAuthorizationHeader(authorizationHeader)) {
log.debug("獲取到認(rèn)證頭Authorization的值:[{}]但不是我們系統(tǒng)中登錄后簽發(fā)的。", authorizationHeader);
filterChain.doFilter(request, response);
return;
}
// 獲取到真實(shí)的token
String realToken = getRealAuthorizationToken(authorizationHeader);
// 解析 jwt token
Jws<Claims> jws = JwtUtils.parserAuthenticateToken(realToken, tokenProperties.getSecretKey());
// token 不合法
if (null == jws) {
writeJson(response, "認(rèn)證token不合法");
return;
}
// token 是否過(guò)期
if (JwtUtils.isJwtExpired(jws)) {
// 處理過(guò)期
handleTokenExpired(response, request, filterChain);
return;
}
// 構(gòu)建認(rèn)證對(duì)象
JwtUtils.buildAuthentication(jws, tokenProperties.getUserId());
filterChain.doFilter(request, response);
}
/**
* 處理token過(guò)期情況
*
* @param response
* @param request
* @param filterChain
* @return
* @throws IOException
*/
private void handleTokenExpired(HttpServletResponse response, HttpServletRequest request, FilterChain filterChain) throws IOException, ServletException {
// 獲取刷新 token
String refreshTokenHeader = request.getHeader(tokenProperties.getRefreshHeaderName());
// 檢測(cè) refresh-token 是否是我們系統(tǒng)中簽發(fā)的
if (!checkIsTokenAuthorizationHeader(refreshTokenHeader)) {
log.debug("獲取到刷新認(rèn)證頭:[{}]的值:[{}]但不是我們系統(tǒng)中登錄后簽發(fā)的。", tokenProperties.getRefreshHeaderName(), refreshTokenHeader);
writeJson(response, "token過(guò)期了,refresh token 不是我們系統(tǒng)簽發(fā)的");
return;
}
// 解析 refresh-token
Jws<Claims> refreshToken = JwtUtils.parserAuthenticateToken(getRealAuthorizationToken(refreshTokenHeader),
tokenProperties.getSecretKey());
// 判斷 refresh-token 是否不合法
if (null == refreshToken) {
writeJson(response, "refresh token不合法");
return;
}
// 判斷 refresh-token 是否過(guò)期
if (JwtUtils.isJwtExpired(refreshToken)) {
writeJson(response, "refresh token 過(guò)期了");
return;
}
// 重新簽發(fā) token
String newToken = JwtUtils.generatorJwtToken(
refreshToken.getBody().get(tokenProperties.getUserId()),
tokenProperties.getUserId(),
tokenProperties.getTokenExpireSecond(),
tokenProperties.getSecretKey()
);
response.addHeader(tokenProperties.getAuthorizationHeaderName(), newToken);
// 構(gòu)建認(rèn)證對(duì)象
JwtUtils.buildAuthentication(JwtUtils.parserAuthenticateToken(newToken, tokenProperties.getSecretKey()), tokenProperties.getUserId());
filterChain.doFilter(request, response);
}
/**
* 寫(xiě) json 數(shù)據(jù)給前端
*
* @param response
* @throws IOException
*/
private void writeJson(HttpServletResponse response, String msg) throws IOException {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, String> params = new HashMap<>(4);
params.put("msg", msg);
response.getWriter().print(OBJECT_MAPPER.writeValueAsString(params));
}
/**
* 獲取到真實(shí)的 token 串
*
* @param authorizationToken
* @return
*/
private String getRealAuthorizationToken(String authorizationToken) {
return StringUtils.substring(authorizationToken, tokenProperties.getTokenHeaderPrefix().length()).trim();
}
/**
* 判斷是否是系統(tǒng)中登錄后簽發(fā)的token
*
* @param authorizationHeader
* @return
*/
private boolean checkIsTokenAuthorizationHeader(String authorizationHeader) {
if (StringUtils.isBlank(authorizationHeader)) {
return false;
}
if (!StringUtils.startsWith(authorizationHeader, tokenProperties.getTokenHeaderPrefix())) {
return false;
}
return true;
}
}
2、jwt 工具類代碼
package com.huan.study.security.token;
import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultJws;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
/**
* jwt 工具類
*
* @author huan
* @date 2020-05-20 - 17:09
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JwtUtils {
/**
* 解析 jwt token
*
* @param token 需要解析的json
* @param secretKey 密鑰
* @return
*/
public static Jws<Claims> parserAuthenticateToken(String token, String secretKey) {
try {
final Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return claimsJws;
} catch (ExpiredJwtException e) {
return new DefaultJws<>(null, e.getClaims(), "");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException | IncorrectClaimException e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 判斷 jwt 是否過(guò)期
*
* @param jws
* @return true:過(guò)期 false:沒(méi)過(guò)期
*/
public static boolean isJwtExpired(Jws<Claims> jws) {
return jws.getBody().getExpiration().before(new Date());
}
/**
* 構(gòu)建認(rèn)證過(guò)的認(rèn)證對(duì)象
*/
public static Authentication buildAuthentication(Jws<Claims> jws, String userIdFieldName) {
Object userId = jws.getBody().get(userIdFieldName);
TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken(userId, null, new ArrayList<>(0));
SecurityContextHolder.getContext().setAuthentication(testingAuthenticationToken);
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 生成 jwt token
*/
public static String generatorJwtToken(Object loginUserId, String userIdFieldName, Long expireSecond, String secretKey) {
Date expireTime = Date.from(LocalDateTime.now().plusSeconds(expireSecond).atZone(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setIssuedAt(new Date())
.setExpiration(expireTime)
.claim(userIdFieldName, loginUserId)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
完整代碼
代碼 https://gitee.com/huan1993/Spring-Security/tree/master/spring-security-jwt