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