前言
用戶鑒權(quán)一直是我先前的一個問題,以前我用戶接口鑒權(quán)是通過傳入?yún)?shù)進(jìn)行鑒權(quán),只要是驗(yàn)證用戶的地方就寫token驗(yàn)證,雖然后面也把token驗(yàn)證方法提取到基類中,但是整體來說仍然不是太雅觀,當(dāng)時的接口如下所示.
@RequestMapping(value = "like",method = RequestMethod.POST)
public ResultMap userLikeOrDisLikeAction(@RequestParam(value = "shopId") String shopId,
@RequestParam(value = "userId") String userId,
@RequestParam(value = "islike") int islike,
@RequestParam(value = "token") String token,
@RequestParam(value = "timestamp") String timestamp
)
{
ResultMap map = new ResultMap();
if (!verifyTokenString(token,timestamp)){
map.code = Constants.ERROR_CODE_TOKEN_NOT_EQUAL;
map.msg = "token錯誤";
return map;
}
....
}
反正一句話來說,自己太菜了...
其實(shí)很久之前,就有了相應(yīng)的解決方案,那就是利用AOP在攔截器中統(tǒng)一處理token校驗(yàn)的問題,那我們一起看看SpringBoot中如何使用JWT來做Token校驗(yàn)和單點(diǎn)登錄的.
JWT集成
項(xiàng)目是基于Maven來架構(gòu)的,所以我們先導(dǎo)入JWT的依賴.整體如下所示.
<!-- JWT的用戶token相關(guān) -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JWT的用戶token相關(guān) -->
對于需要創(chuàng)建的類來說,主要有以下幾個類.
下面我們簡單看一下各個文件的作用.
InterceptorConfig : Spring boot2.0 官方推薦實(shí)現(xiàn) WebMvcConfigurer 接口配置攔截器.
JwtConfig : token的相關(guān)方法工具類.
TokenInterceptor : 攔截器
PassToken 、UserLoginToken : 自定義注解,用于標(biāo)注接口或者類是否需要進(jìn)行token驗(yàn)證.
具體代碼
首先,我們對上面的類或者注解進(jìn)行一個詳細(xì)的說明.
InterceptorConfig
該類主要是用來配置攔截器的,具體代碼如下所示.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**");
}
}
JwtConfig
該類主要是用來定義token的相關(guān)方法.例如,創(chuàng)建token,創(chuàng)建刷新token等等,驗(yàn)證token是否過期,獲取token中的用戶信息等等.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtConfig {
private static final Log log = LogFactory.getLog(JwtConfig.class);
private String secret = "秘鑰,請自己定義";
// 外部http請求中 header中 token的 鍵值
private String header = "token";
private static Map<String, String> tokenMap = new HashMap<>();
/**
* 生成token
*
* @param subject
* @return
*/
public String createToken(String subject) {
Date nowDate = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(nowDate);
calendar.add(Calendar.DAY_OF_MONTH, 10);
Date expireDate = calendar.getTime();
String userToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
// 把token添加到緩存中
tokenMap.put(subject, userToken);
return userToken;
}
public String createRefreshToken(String subject) {
Date nowDate = new Date();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 獲取token中注冊信息
*
* @param token
* @return
*/
public Claims getTokenClaim(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
return null;
}
}
/**
* 驗(yàn)證token是否過期失效
*
* @param expirationTime
* @return
*/
public boolean isTokenExpired(Date expirationTime) {
return expirationTime.before(new Date());
}
/**
* 獲取token失效時間
*
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getTokenClaim(token).getExpiration();
}
/**
* 獲取用戶名從token中
*/
public String getUsernameFromToken(String token) {
return getTokenClaim(token).getSubject();
}
/**
* 獲取jwt發(fā)布時間
*/
public Date getIssuedAtDateFromToken(String token) {
return getTokenClaim(token).getIssuedAt();
}
// --------------------- getter & setter ---------------------
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public Map<String, String> getTokenMap() {
return tokenMap;
}
}
PassToken
定義一個哪些類或者接口跳過驗(yàn)證的注解,不添加也也判定是跳過驗(yàn)證.具體實(shí)現(xiàn)代碼如下所示.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
UserLoginToken
定義一個哪些類或者接口需要驗(yàn)證的注解,具體實(shí)現(xiàn)代碼如下所示.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
TokenInterceptor
攔截器,繼承于 HandlerInterceptorAdapter 這個抽象類, 實(shí)現(xiàn)接口攔截驗(yàn)證功能,具體代碼如下所示.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Resource
private JwtConfig jwtConfig;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws SignatureException, IOException {
String uri = request.getRequestURI();
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
/** 檢查是否有passtoken注釋好爬,有則跳過認(rèn)證 */
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
/** 檢查有沒有需要用戶權(quán)限的注解 */
if (method.isAnnotationPresent(UserLoginToken.class)) {
/** Token 驗(yàn)證 */
String token = request.getHeader(jwtConfig.getHeader());
if (StringUtils.isEmpty(token)) {
token = request.getParameter(jwtConfig.getHeader());
}
if (StringUtils.isEmpty(token)) {
response.sendError(401, "token信息不能為空");
return false;
}
String userName = jwtConfig.getUsernameFromToken(token);
String compareToken = jwtConfig.getTokenMap().get(userName);
if (compareToken != null && !compareToken.equals(token)) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
Claims claims = null;
try {
claims = jwtConfig.getTokenClaim(token);
if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
} catch (Exception e) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
/** 設(shè)置 identityId 用戶身份ID */
request.setAttribute("identityId", claims.getSubject());
return true;
}
if (compareToken == null) {
// 由于服務(wù)器war重新上傳導(dǎo)致臨時數(shù)據(jù)丟失,需要重新存儲
jwtConfig.getTokenMap().put(userName, token);
}
}
return true;
}
}
Token驗(yàn)證
Token驗(yàn)證的過程主要是在攔截器中,用戶在登錄過程中,我們需要把生成好的token 局雄、refreshToken(刷新token)、expirationDate(過期時間)發(fā)送給用戶.然后再需要的接口的header中傳入token信息用于驗(yàn)證.
驗(yàn)證過程主要是在 preHandle 方法中實(shí)現(xiàn)的.
首先我們驗(yàn)證是否含有 @PassToken 這個注解,如果有,那么直接跳過驗(yàn)證.
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
然后只有含有 @UserLoginToken 的接口中才去驗(yàn)證token.驗(yàn)證Token主要是驗(yàn)證它的過期時間.代碼如下所示.
if (userLoginToken.required()) {
Claims claims = null;
try {
claims = jwtConfig.getTokenClaim(token);
if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
} catch (Exception e) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
/** 設(shè)置 identityId 用戶身份ID */
request.setAttribute("identityId", claims.getSubject());
return true;
}
單點(diǎn)登錄
如何簡單實(shí)現(xiàn)一個單點(diǎn)登錄呢?我們需要維護(hù)一個全局的HaspMap,以 Token中的 subject (這里我使用的不會重復(fù)的username) 作為鍵值,以token為value存儲. Map定義在 JwtConfig 中,代碼如下所示.
private static Map<String, String> tokenMap = new HashMap<>();
在創(chuàng)建token的方法中,我們認(rèn)定前面的token都失效了,所以我們直接添加即可,如果存在舊的token就進(jìn)行覆蓋操作,如果沒有就進(jìn)行添加.代碼如下所示.
public String createToken(String subject) {
....
String userToken = ....
tokenMap.put(subject, userToken);
....
}
在攔截器中的攔截方法中我們需要去驗(yàn)證 傳入的token是否是我們存儲中的token,如果不是,那么就直接返回token過期.
String userName = jwtConfig.getUsernameFromToken(token);
String compareToken = jwtConfig.getTokenMap().get(userName);
if (compareToken != null && !compareToken.equals(token)) {
response.sendError(400, "token已經(jīng)失效,請重新登錄");
return false;
}
由于HashMap存儲在緩存中,當(dāng)下次服務(wù)重啟的時候,HashMap所有值就會失效.這時候我們該如何做呢?我們需要在攔截方法最后把當(dāng)前驗(yàn)證完畢的token 重新填入 Map中即可.
if (compareToken == null) {
// 由于服務(wù)器war重新上傳導(dǎo)致臨時數(shù)據(jù)丟失,需要重新存儲
jwtConfig.getTokenMap().put(userName, token);
}
刷新token
當(dāng)token過期之后,我們允許用戶進(jìn)行token的刷新.這時候我們需要定義一個生成刷新token的方法,如下所示.
public String createRefreshToken(String subject) {
Date nowDate = new Date();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
我們已經(jīng)在登錄之時把該refreshToken 返回給用戶,只要我們定義接口實(shí)現(xiàn)新token的創(chuàng)建即可.這樣就完成token的刷新了.
結(jié)語
基于JWT的token校驗(yàn)存炮、單點(diǎn)登錄炬搭、刷新token整體來說還是比較簡單的,如果有問題,歡迎各位大佬在評論區(qū)指導(dǎo)批評,謝謝啦~OK,今天就到這里了.....