文章目錄
1 摘要
接口的權限認證能夠有效保護接口不被濫用。常用的REST風格接口權限認證方式有簽名校驗和基於Auth2.0的Token校驗方式。本文將介紹基於 JWT 實現的接口 Token 認證解決方案1.0。
2 需求分析
接口認證需求:
- 1 能夠有選擇地過濾沒有權限(Token)的請求
- 2 Token 具有時效性
- 3 如果用戶連續操作,Token 能夠自動刷新(自動延長有效期,eg: 加入 Token 有效期爲 10 小時,若用戶在 10 小時以內有操作,則在時間到達 10 時,Token 能夠自動刷新,而不是失效,從而影響用戶體驗)
解決思路:
- 1 使用過濾器,可針對需要的接口進行權限認證,對於不需要校驗的接口進行放行
- 2 使用 JWT 設置 Token 有效期
- 3 用戶每次操作都返回最新的 Token ,用戶的新操作都基於上一次請求返回的 Token(Token 放在 Response Header 中,前端自行獲取)
- 4 對 Token 設置刷新時間(刷新時間
<
有效時間),當 Token 的時間小於刷新時間時, Token 不用更新,可連續使用;當 Token 時間超過刷新時間,但是在有效時間以內時,刷新 Token ,刷新 Token 的有效期從刷新時間開始算起,往後延長有效時間,返回用戶最新 Token,當 Token 操作時間在有效時間以外時,提示 Token 失效。
3 核心依賴
../pom.xml
../demo-common/pom.xml
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${auth.jwt.version}</version>
</dependency>
其中 ${auth.jwt.version}
版本爲 3.8.3
4 核心代碼
4.1 JWT Token 生成與校驗工具類
../demo-common/src/main/java/com/ljq/demo/springboot/common/util/JwtUtil.java
package com.ljq.demo.springboot.common.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: JWT 工具類
* @Author: junqiang.lu
* @Date: 2019/12/3
*/
public class JwtUtil implements Serializable {
private static final long serialVersionUID = -9101115541530000111L;
/**
* 默認祕鑰
*/
private final static String DEFAULT_SECRET = "TS2XIUYNOKJZZDXD8YA9JJH5PM1IAHXPYCX7Q3JO";
private JwtUtil(){
}
/**
* 加密
*
* @param key
* @param value
* @return
* @throws JWTCreationException
*/
public static String encode(String key, String value) throws Exception {
return encode(key, value, 0);
}
/**
* 加密
*
* @param key
* @param value
* @param expTime
* @return
* @throws JWTCreationException
*/
public static String encode(String key, String value, long expTime) throws Exception {
return encode(null, key, value, expTime);
}
/**
* 加密
*
* @param secret
* @param key
* @param value
* @param expMillis
* @return
*/
public static String encode(String secret, String key, String value, long expMillis) throws Exception {
if (secret == null || secret.length() < 1) {
secret = DEFAULT_SECRET;
}
Date expDate = null;
if (expMillis > 1) {
expDate = new Date(System.currentTimeMillis() + expMillis);
}
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withIssuer("auth0")
.withClaim(key,value)
.withExpiresAt(expDate)
.sign(algorithm);
return token;
}
/**
* 解密
*
* @param key
* @param encodedToken
* @return
* @throws JWTDecodeException
*/
public static String decode(String key, String encodedToken) throws Exception {
return decode(null, key, encodedToken);
}
/**
* 解密
*
* @param secret
* @param key
* @param encodedToken
* @return
*/
public static String decode(String secret, String key, String encodedToken) throws Exception {
if (secret == null || secret.length() < 1) {
secret = DEFAULT_SECRET;
}
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build();
return verifier.verify(encodedToken).getClaim(key).asString();
}
}
4.2 Token 攔截器
../demo-web/src/main/java/com/ljq/demo/springboot/web/acpect/SimpleCORSFilter.java
package com.ljq.demo.springboot.web.acpect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.constant.TokenConst;
import com.ljq.demo.springboot.common.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
/**
* @Description: 跨域請求攔截器(簡易版)
* @Author: junqiang.lu
* @Date: 2019/5/21
*/
@Slf4j
public class SimpleCORSFilter implements Filter {
/**
* 不需要 Token 校驗的接口
*/
private final static String[] NO_TOKEN_API_PATHS ={
"/api/rest/user/save",
"/api/rest/user/info"
};
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
// *表示允許所有域名跨域
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
httpResponse.addHeader("Access-Control-Allow-Headers","*");
// 允許跨域的Http方法
httpResponse.addHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE");
// 允許瀏覽器訪問 Token 認證響應頭
httpResponse.addHeader("Access-Control-Expose-Headers", TokenConst.TOKEN_HEADERS_FIELD);
// 默認返回原 Token
httpResponse.setHeader(TokenConst.TOKEN_HEADERS_FIELD, httpRequest.getHeader(TokenConst.TOKEN_HEADERS_FIELD));
// 應對探針模式請求(OPTIONS)
String methodOptions = "OPTIONS";
if (httpRequest.getMethod().equals(methodOptions)) {
httpResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
return;
}
/**
* 校驗用戶 Token
*/
boolean flag = !Arrays.asList(NO_TOKEN_API_PATHS).contains(httpRequest.getRequestURI());
if (flag) {
ResponseCode responseCode = checkToken(httpRequest, httpResponse);
if (!Objects.equals(responseCode, ResponseCode.SUCCESS)) {
log.warn("{}", responseCode);
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.setCharacterEncoding("utf-8");
PrintWriter writer = httpResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString(ApiResult.failure(responseCode)));
return;
}
}
filterChain.doFilter(httpRequest, servletResponse);
}
@Override
public void destroy() {
}
/**
* 用戶 Token 校驗
*
* @param request
* @param response
* @return
*/
private ResponseCode checkToken(HttpServletRequest request, HttpServletResponse response) {
try {
String token = request.getHeader(TokenConst.TOKEN_HEADERS_FIELD);
if (token == null || token.length() < 1) {
return ResponseCode.USER_TOKEN_NULL_ERROR;
}
String tokenValue = JwtUtil.decode(TokenConst.TOKEN_KEY, token);
long time = Long.parseLong(tokenValue.substring(tokenValue.indexOf("@") + 1));
String userPhone = tokenValue.substring(0, tokenValue.indexOf("@"));
log.info("{}, date: {}, userPhone: {}", tokenValue, new Date(time), userPhone);
// 校驗 Token 有效性
long subResult = System.currentTimeMillis() - time;
if (subResult >= TokenConst.TOKEN_EXPIRE_TIME_MILLIS) {
return ResponseCode.USER_TOKEN_ERROR;
}
if (subResult < TokenConst.TOKEN_REFRESH_TIME_MILLIS) {
return ResponseCode.SUCCESS;
}
// 刷新 Token
String newToken = JwtUtil.encode(TokenConst.TOKEN_KEY,userPhone + "@" + System.currentTimeMillis());
response.setHeader(TokenConst.TOKEN_HEADERS_FIELD, newToken);
} catch (Exception e) {
log.warn("Token 校驗失敗,{}:{}", e.getClass().getName(), e.getMessage());
return ResponseCode.USER_TOKEN_ERROR;
}
return ResponseCode.SUCCESS;
}
}
其中 NO_TOKEN_API_PATHS
爲一些不需要 Token 校驗的接口
4.3 Token 相關的常量
../demo-common/src/main/java/com/ljq/demo/springboot/common/constant/TokenConst.java
package com.ljq.demo.springboot.common.constant;
/**
* @Description: Token 相關常量
* @Author: junqiang.lu
* @Date: 2019/12/3
*/
public class TokenConst {
/**
* Token headers 字段
*/
public static final String TOKEN_HEADERS_FIELD = "Authorization";
/**
* token key
*/
public static final String TOKEN_KEY = "tokenPhone";
/**
* Token 刷新時間(單位: 毫秒)
*/
public static final long TOKEN_REFRESH_TIME_MILLIS = 1000 * 60 * 60 * 2L;
/**
* token 有效期(單位: 毫秒)
*/
public static final long TOKEN_EXPIRE_TIME_MILLIS = 1000 * 60 * 60 * 24 * 30L;
}
4.4 其他相關類
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCode.java
5 測試
Token 生成,參考:
com.ljq.demo.springboot.common.util.JwtUtil#encode(java.lang.String, java.lang.String)
5.1 不需要 Token
請求接口:
/api/rest/user/save
請求參數&返回結果:
5.2 需要 Token,請求成功
請求接口:
/api/rest/user/save2
請求參數&返回結果:
5.3 需要 Token,請求失敗
請求接口:
/api/rest/user/save2
請求參數與返回結果:
6 參考資料推薦
7 Github 源碼
Gtihub 源碼地址 : https://github.com/Flying9001/springBootDemo
個人公衆號:404Code,分享半個互聯網人的技術與思考,感興趣的可以關注.