這兩天在寫專案的全域性許可權校驗,用 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後端的全平臺型全棧專案,歡迎關注。
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~