服務閘道器 Zuul 與 Redis 結合實現 Token 許可權校驗
這兩天在寫專案的全域性許可權校驗,用 Zuul 作為服務閘道器,在 Zuul 的前置過濾器裡做的校驗。
許可權校驗或者身份驗證就不得不提 Token,目前 Token 的驗證方式有很多種,有生成 Token 後將 Token 儲存在 Redis 或資料庫的,也有很多用 JWT(JSON Web Token)的。
說實話這方面我的經驗不多,又著急趕專案,所以就先用個簡單的方案。
登入成功後將 Token 返回給前端,同時將 Token 存在 Redis 裡。每次請求介面都從 Cookie 或 Header 中取出 Token,在從 Redis 中取出儲存的 Token,比對是否一致。
我知道這方案不是最完美的,還有安全性問題,容易被劫持。但目前的策略是先把專案功能做完,上線之後再慢慢優化,不在一個功能點上扣的太細,保證專案進度不至於太慢。
本文將分四部分介紹
- 登入邏輯
- AuthFilter 前置過濾器校驗邏輯
- 工具類
- 演示驗證
一、登入邏輯
登入成功後,將生成的 Token 儲存在 Redis 中。用 String 型別的 key, value 格式儲存,key是 TOKEN_userId
,如果使用者的 userId 是 222222
,那鍵就是 TOKEN_222222
;值是生成的 Token。
只貼出登入的 Serive 程式碼
@Override
public UserInfoDTO loginByEmail(String email, String password) {
if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
throw new UserException(ResultEnum.EMAIL_PASSWORD_EMPTY);
}
UserInfo user = userRepository.findUserInfoByEmail(email);
if (user == null) {
throw new UserException(ResultEnum.EMAIL_NOT_EXIST);
}
if (!user.getPassword().equals(password)) {
throw new UserException(ResultEnum.PASSWORD_ERROR);
}
//生成 token 並儲存在 Redis 中
String token = KeyUtils.genUniqueKey();
//將token儲存在 Redis 中。鍵是 TOKEN_使用者id, 值是token
redisUtils.setString(String.format(RedisConsts.TOKEN_TEMPLATE, user.getId()), token, 2l, TimeUnit.HOURS);
UserInfoDTO dto = new UserInfoDTO();
BeanUtils.copyProperties(user, dto);
dto.setToken(token);
return dto;
}
複製程式碼
二、AuthFilter 前置過濾器
AuthFilter
繼承自 ZuulFilter
,必須實現 ZuulFilter
的四個方法。
filterType()
: Filter 的型別,前置過濾器返回PRE_TYPE
filterOrder()
: Filter 的順序,值越小越先執行。這裡的寫法是PRE_DECORATION_FILTER_ORDER - 1
, 也是官方建議的寫法。
shouldFilter()
: 是否應該過濾。返回 true 表示過濾,false 不過濾。可以在這個方法裡判斷哪些介面不需要過濾,本例排除了註冊和登入介面,除了這兩個介面,其他的都需要過濾。
run()
: 過濾器的具體邏輯
為了方便前端,考慮到要給 pc、app、小程式等不同平臺提供服務,token 設定在 cookie 和 header 任選一均可,會先從 cookie 中取,cookie 中沒有再從 header 中取。
package com.solo.coderiver.gateway.filter;
import com.google.gson.Gson;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.solo.coderiver.gateway.VO.ResultVO;
import com.solo.coderiver.gateway.consts.RedisConsts;
import com.solo.coderiver.gateway.utils.CookieUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* 許可權驗證 Filter
* 註冊和登入介面不過濾
*
* 驗證許可權需要前端在 Cookie 或 Header 中(二選一即可)設定使用者的 userId 和 token
* 因為 token 是存在 Redis 中的,Redis 的鍵由 userId 構成,值是 token
* 在兩個地方都沒有找打 userId 或 token其中之一,就會返回 401 無許可權,並給與文字提示
*/
@Slf4j
@Component
public class AuthFilter extends ZuulFilter {
@Autowired
StringRedisTemplate stringRedisTemplate;
//排除過濾的 uri 地址
private static final String LOGIN_URI = "/user/user/login";
private static final String REGISTER_URI = "/user/user/register";
//無許可權時的提示語
private static final String INVALID_TOKEN = "invalid token";
private static final String INVALID_USERID = "invalid userId";
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
log.info("uri:{}", request.getRequestURI());
//註冊和登入介面不攔截,其他介面都要攔截校驗 token
if (LOGIN_URI.equals(request.getRequestURI()) ||
REGISTER_URI.equals(request.getRequestURI())) {
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//先從 cookie 中取 token,cookie 中取失敗再從 header 中取,兩重校驗
//通過工具類從 Cookie 中取出 token
Cookie tokenCookie = CookieUtils.getCookieByName(request, "token");
if (tokenCookie == null || StringUtils.isEmpty(tokenCookie.getValue())) {
readTokenFromHeader(requestContext, request);
} else {
verifyToken(requestContext, request, tokenCookie.getValue());
}
return null;
}
/**
* 從 header 中讀取 token 並校驗
*/
private void readTokenFromHeader(RequestContext requestContext, HttpServletRequest request) {
//從 header 中讀取
String headerToken = request.getHeader("token");
if (StringUtils.isEmpty(headerToken)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
} else {
verifyToken(requestContext, request, headerToken);
}
}
/**
* 從Redis中校驗token
*/
private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) {
//需要從cookie或header 中取出 userId 來校驗 token 的有效性,因為每個使用者對應一個token,在Redis中是以 TOKEN_userId 為鍵的
Cookie userIdCookie = CookieUtils.getCookieByName(request, "userId");
if (userIdCookie == null || StringUtils.isEmpty(userIdCookie.getValue())) {
//從header中取userId
String userId = request.getHeader("userId");
if (StringUtils.isEmpty(userId)) {
setUnauthorizedResponse(requestContext, INVALID_USERID);
} else {
String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userId));
if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
}
}
} else {
String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userIdCookie.getValue()));
if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
}
}
}
/**
* 設定 401 無許可權狀態
*/
private void setUnauthorizedResponse(RequestContext requestContext, String msg) {
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
ResultVO vo = new ResultVO();
vo.setCode(401);
vo.setMsg(msg);
Gson gson = new Gson();
String result = gson.toJson(vo);
requestContext.setResponseBody(result);
}
}
複製程式碼
三、工具類
MD5 工具類
package com.solo.coderiver.user.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 生成 MD5 的工具類
*/
public class MD5Utils {
public static String getMd5(String plainText) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(plainText.getBytes());
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
/**
* 加密解密演算法 執行一次加密,兩次解密
*/
public static String convertMD5(String inStr){
char[] a = inStr.toCharArray();
for (int i = 0; i < a.length; i++){
a[i] = (char) (a[i] ^ 't');
}
String s = new String(a);
return s;
}
}
複製程式碼
生成 key 的工具類
package com.solo.coderiver.user.utils;
import java.util.Random;
public class KeyUtils {
/**
* 產生獨一無二的key
*/
public static synchronized String genUniqueKey(){
Random random = new Random();
int number = random.nextInt(900000) + 100000;
String key = System.currentTimeMillis() + String.valueOf(number);
return MD5Utils.getMd5(key);
}
}
複製程式碼
四、演示驗證
在 8084 埠啟動 api_gateway
專案,同時啟動 user
專案。
用 postman 通過閘道器訪問登入介面,因為過濾器對登入和註冊介面排除了,所以不會校驗這兩個介面的 token。
可以看到,訪問地址 http://localhost:8084/user/user/login
登入成功並返回了使用者資訊和 token。
此時應該把 token 存入 Redis 中了,使用者的 id 是 111111
,所以鍵是 TOKEN_111111
,值是剛生成的 token 值
再來隨便請求一個其他的介面,應該走過濾器。
header 中不傳 token 和 userId,返回 401
只傳 token 不傳 userId,返回401並提示 invalid userId
token 和 userId 都傳,但 token 不對,返回401,並提示 invalid token
同時傳正確的 token 和 userId,請求成功
以上就是簡單的 Token 校驗,如果有更好的方案歡迎在評論區交流
程式碼出自開源專案 CodeRiver
,致力於打造全平臺型全棧精品開源專案。
coderiver 中文名 河碼,是一個為程式設計師和設計師提供專案協作的平臺。無論你是前端、後端、移動端開發人員,或是設計師、產品經理,都可以在平臺上釋出專案,與志同道合的小夥伴一起協作完成專案。
coderiver河碼 類似程式設計師客棧,但主要目的是方便各細分領域人才之間技術交流,共同成長,多人協作完成專案。暫不涉及金錢交易。
計劃做成包含 pc端(Vue、React)、移動H5(Vue、React)、ReactNative混合開發、Android原生、微信小程式、java後端的全平臺型全棧專案,歡迎關注。
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~