這兩天在寫項目的全局權限校驗,用 Zuul 做爲服務網關,在 Zuul 的前置過濾器裏作的校驗。前端
權限校驗或者身份驗證就不得不提 Token,目前 Token 的驗證方式有不少種,有生成 Token 後將 Token 存儲在 Redis 或數據庫的,也有不少用 JWT(JSON Web Token)的。java
說實話這方面個人經驗很少,又着急趕項目,因此就先用個簡單的方案。git
登陸成功後將 Token 返回給前端,同時將 Token 存在 Redis 裏。每次請求接口都從 Cookie 或 Header 中取出 Token,在從 Redis 中取出存儲的 Token,比對是否一致。程序員
我知道這方案不是最完美的,還有安全性問題,容易被劫持。但目前的策略是先把項目功能作完,上線以後再慢慢優化,不在一個功能點上扣的太細,保證項目進度不至於太慢。github
項目地址:github.com/cachecats/c…redis
本文將分四部分介紹算法
登陸成功後,將生成的 Token 存儲在 Redis 中。用 String 類型的 key, value 格式存儲,key是 TOKEN_userId
,若是用戶的 userId 是 222222
,那鍵就是 TOKEN_222222
;值是生成的 Token。spring
只貼出登陸的 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
繼承自 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後端的全平臺型全棧項目,歡迎關注。
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~