前言 這兩三年項目中一直在使用比較流行的spring cloud框架,也算有一定積累,打算有時間就整理一些乾貨與大家分享。 本次分享zuul網關集成jwt身份驗證 業務背景 項目開發少不了身份認證,jwt作為當下比較流行的身份認證方式之一主要的特點是無狀態,把信息放在客戶端,伺服器端不需要保存ses ...
前言
這兩三年項目中一直在使用比較流行的spring cloud框架,也算有一定積累,打算有時間就整理一些乾貨與大家分享。
本次分享zuul網關集成jwt身份驗證
業務背景
項目開發少不了身份認證,jwt作為當下比較流行的身份認證方式之一主要的特點是無狀態,把信息放在客戶端,伺服器端不需要保存session,適合分散式系統使用。
把jwt集成在網關的好處是業務工程不需要關心身份驗證,專註業務邏輯(網關可驗證token後,把解析出來的身份信息如userId,放在請求頭傳遞給業務工程)。
順便分享下如何自定義Zuul攔截器
代碼詳解
一、JwtUtil
為了方便,先封裝好JwtUtil,主要包含兩個方法,創建token和解析(並驗證)token
這裡引用了第三方的包jjwt,簡單好用,maven依賴如下
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jwtUtil封裝如下
@Component
public class JwtUtil {
/**
* 簽名用的密鑰
*/
private static final String SIGNING_KEY = "78sebr72umyz33i9876gc31urjgyfhgj";
/**
* 用戶登錄成功後生成Jwt
* 使用Hs256演算法
*
* @param exp jwt過期時間
* @param claims 保存在Payload(有效載荷)中的內容
* @return token字元串
*/
public String createJWT(Date exp, Map<String, Object> claims) {
//指定簽名的時候使用的簽名演算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//創建一個JwtBuilder,設置jwt的body
JwtBuilder builder = Jwts.builder()
//保存在Payload(有效載荷)中的內容
.setClaims(claims)
//iat: jwt的簽發時間
.setIssuedAt(now)
//設置過期時間
.setExpiration(exp)
//設置簽名使用的簽名演算法和簽名使用的秘鑰
.signWith(signatureAlgorithm, SIGNING_KEY);
return builder.compact();
}
/**
* 解析token,獲取到Payload(有效載荷)中的內容,包括驗證簽名,判斷是否過期
*
* @param token
* @return
*/
public Claims parseJWT(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//設置簽名的秘鑰
.setSigningKey(SIGNING_KEY)
//設置需要解析的token
.parseClaimsJws(token).getBody();
return claims;
}
}
二、自定義攔截器說明
繼承自ZuulFilter,並註冊到spring容器即可實現自定義攔截器,實現身份認證、參數校驗、參數傳遞等功能
@Component
public class CustomFilter extends ZuulFilter {
/**
* filterType:過濾器類型
* <p>
* pre:路由之前
* routing:路由之時
* post: 路由之後
* error:發送錯誤調用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
// return FilterConstants.POST_TYPE;
}
/**
* filterOrder:過濾的順序 序號配置可參照 https://blog.csdn.net/u010963948/article/details/100146656
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* shouldFilter:判斷是否要執行過濾
*
* @return true表示需要過濾,將對該請求執行run方法
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* run:具體過濾的業務邏輯,可做身份驗證,校驗參數等等
*
* @return
*/
@Override
public Object run() throws ZuulException {
//獲取請求上下文對象
RequestContext ctx = RequestContext.getCurrentContext();
//獲取request對象
HttpServletRequest request = ctx.getRequest();
//獲取response對象
HttpServletResponse response = ctx.getResponse();
//添加請求頭,傳遞到業務服務
ctx.addZuulRequestHeader("xxx", "xxx");
//添加響應頭,返回給前端
ctx.addZuulResponseHeader("xxx", "xxx");
return null;
}
}
三、LoginAddJwtPostFilter,攔截登錄方法,登錄成功時創建token,返回給前端
要點:
- 攔截類型是FilterConstants.POST_TYPE,在路由方法響應之後攔截
- 判斷請求的uri是否是登錄介面(與配置文件中設置的登錄uri是否匹配),需要在配置文件配置登錄介面地址
- 判斷登錄方法返回成功,創建token,並添加到 response body或response header,返回給前端
@Component
@Slf4j
public class LoginAddJwtPostFilter extends ZuulFilter {
@Autowired
ObjectMapper objectMapper;
@Autowired
JwtUtil jwtUtil;
@Autowired
DataFilterConfig dataFilterConfig;
/**
* pre:路由之前
* routing:路由之時
* post: 路由之後
* error:發送錯誤調用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
/**
* filterOrder:過濾的順序
*
* @return
*/
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 2;
}
/**
* shouldFilter:這裡可以寫邏輯判斷,是否要過濾
*
* @return
*/
@Override
public boolean shouldFilter() {
//路徑與配置的相匹配,則執行過濾
RequestContext ctx = RequestContext.getCurrentContext();
for (String pathPattern : dataFilterConfig.getUserLoginPath()) {
if (PathUtil.isPathMatch(pathPattern, ctx.getRequest().getRequestURI())) {
return true;
}
}
return false;
}
/**
* 執行過濾器邏輯,登錄成功時給響應內容增加token
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
InputStream stream = ctx.getResponseDataStream();
String body = StreamUtils.copyToString(stream, StandardCharsets.UTF_8);
Result<HashMap<String, Object>> result = objectMapper.readValue(body, new TypeReference<Result<HashMap<String, Object>>>() {
});
//result.getCode() == 0 表示登錄成功
if (result.getCode() == 0) {
HashMap<String, Object> jwtClaims = new HashMap<String, Object>() {{
put("userId", result.getData().get("userId"));
}};
Date expDate = DateTime.now().plusDays(7).toDate(); //過期時間 7 天
String token = jwtUtil.createJWT(expDate, jwtClaims);
//body json增加token
result.getData().put("token", token);
//序列化body json,設置到響應body中
body = objectMapper.writeValueAsString(result);
ctx.setResponseBody(body);
//響應頭設置token
ctx.addZuulResponseHeader("token", token);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
四、JwtAuthPreFilter,攔截業務介面,驗證token
要點:
- 攔截類型是FilterConstants.PRE_TYPE,在調用業務介面之前攔截
- 判斷請求的uri是否是需要身份驗證的介面(與配置文件中設置的uri是否匹配),需要在配置文件配置業務介面地址
- 判斷token驗證是否通過,通過則路由,不通過返回錯誤提示
@Component
@Slf4j
public class JwtAuthPreFilter extends ZuulFilter {
@Autowired
ObjectMapper objectMapper;
@Autowired
JwtUtil jwtUtil;
@Autowired
DataFilterConfig dataFilterConfig;
/**
* pre:路由之前
* routing:路由之時
* post: 路由之後
* error:發送錯誤調用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* filterOrder:過濾的順序 序號配置可參照 https://blog.csdn.net/u010963948/article/details/100146656
*
* @return
*/
@Override
public int filterOrder() {
return 2;
}
/**
* shouldFilter:邏輯是否要過濾
*
* @return
*/
@Override
public boolean shouldFilter() {
//路徑與配置的相匹配,則執行過濾
RequestContext ctx = RequestContext.getCurrentContext();
for (String pathPattern : dataFilterConfig.getAuthPath()) {
if (PathUtil.isPathMatch(pathPattern, ctx.getRequest().getRequestURI())) {
return true;
}
}
return false;
}
/**
* 執行過濾器邏輯,驗證token
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("token");
Claims claims;
try {
//解析沒有異常則表示token驗證通過,如有必要可根據自身需求增加驗證邏輯
claims = jwtUtil.parseJWT(token);
log.info("token : {} 驗證通過", token);
//對請求進行路由
ctx.setSendZuulResponse(true);
//請求頭加入userId,傳給業務服務
ctx.addZuulRequestHeader("userId", claims.get("userId").toString());
} catch (ExpiredJwtException expiredJwtEx) {
log.error("token : {} 過期", token );
//不對請求進行路由
ctx.setSendZuulResponse(false);
responseError(ctx, -402, "token expired");
} catch (Exception ex) {
log.error("token : {} 驗證失敗" , token );
//不對請求進行路由
ctx.setSendZuulResponse(false);
responseError(ctx, -401, "invalid token");
}
return null;
}
/**
* 將異常信息響應給前端
*/
private void responseError(RequestContext ctx, int code, String message) {
HttpServletResponse response = ctx.getResponse();
Result errResult = new Result();
errResult.setCode(code);
errResult.setMessage(message);
ctx.setResponseBody(toJsonString(errResult));
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=utf-8");
}
private String toJsonString(Object o) {
try {
return objectMapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
log.error("json序列化失敗", e);
return null;
}
}
}
五、配置文件和路徑匹配
在配置文件application.yml中配置登錄介面路徑 和 業務介面(需要身份驗證的介面)路徑,可配置多個,可使用通配符(基於Ant path匹配)
data-filter:
auth-path: #需要驗證token的請求地址,可設置多個,會觸發JwtAuthPreFilter
- /business/data/**
- /business/report/**
user-login-path: #登錄請求地址,可設置多個,會觸發LoginAddJwtPostFilter
- /business/login/**
PathUtil,封裝路徑匹配方法,用於判斷請求的介面是否是需要攔截的介面
public class PathUtil {
private static AntPathMatcher matcher = new AntPathMatcher();
public static boolean isPathMatch(String pattern, String path) {
return matcher.match(pattern, path);
}
}
請求測試
一、測試登錄介面
請求登錄介面 http://localhost:8040/business/login/loginByPassword
看到響應body和header里都有了token:eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzYzMTA3MDgsInVzZXJJZCI6IjEwMDEiLCJpYXQiOjE1NzU3MDU5MDl9.06MmrKGs5MK3nW5m6EaQTkkBviXQccPG33Nx1aF5zFw
把token的第二段 eyJleHAiOjE1NzYzMTA3MDgsInVzZXJJZCI6IjEwMDEiLCJpYXQiOjE1NzU3MDU5MDl9 使用base64解碼
可以看到明文{"exp":1576310708,"userId":"1001","iat":1575705909}
包含了過期時間、用戶id、簽發時間
二、測試業務介面
請求業務介面 http://localhost:8040/business/data/getData 請求頭不傳token或傳錯誤的token
可以看到返回了錯誤信息
{
"code": -401,
"message": "invalid token",
"data": null
}
請求業務介面 http://localhost:8040/business/data/getData 傳入正確的token
可以看到返回了業務數據,說明已經請求到了業務介面,驗證成功