摘要:項目地址本文將分四部分介紹登錄邏輯前置過濾器校驗邏輯工具類演示驗證一登錄邏輯登錄成功后,將生成的存儲在中。鍵是用戶值是二前置過濾器繼承自,必須實現的四個方法。
這兩天在寫項目的全局權限校驗,用 Zuul 作為服務網關,在 Zuul 的前置過濾器里做的校驗。
權限校驗或者身份驗證就不得不提 Token,目前 Token 的驗證方式有很多種,有生成 Token 后將 Token 存儲在 Redis 或數據庫的,也有很多用 JWT(JSON Web Token)的。
說實話這方面我的經驗不多,又著急趕項目,所以就先用個簡單的方案。
登錄成功后將 Token 返回給前端,同時將 Token 存在 Redis 里。每次請求接口都從 Cookie 或 Header 中取出 Token,在從 Redis 中取出存儲的 Token,比對是否一致。
我知道這方案不是最完美的,還有安全性問題,容易被劫持。但目前的策略是先把項目功能做完,上線之后再慢慢優化,不在一個功能點上扣的太細,保證項目進度不至于太慢。
項目地址:https://github.com/cachecats/...
本文將分四部分介紹
登錄邏輯
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_TYPEfilterOrder(): 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/...
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星? ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72270.html
摘要:無論你是前端后端移動端開發人員,或是設計師產品經理,都可以在平臺上發布項目,與志同道合的小伙伴一起協作完成項目。 全平臺全棧開源項目 coderiver 今天終于開始前后端聯調了~ 首先感謝大家的支持,coderiver 在 GitHub 上開源兩周,獲得了 54 個 Star,9 個 Fork,5 個 Watch。 這些鼓勵和認可也更加堅定了我繼續寫下去的決心~ 再次感謝各位大佬! ...
摘要:負載均衡組件是一個負載均衡組件,它通常和配合使用。和配合,很容易做到負載均衡,將請求根據負載均衡策略分配到不同的服務實例中。和配合,在消費服務時能夠做到負載均衡。在默認的情況下,和相結合,能夠做到負載均衡智能路由。 2.2.1 簡介 Spring Cloud 是基于 Spring Boot 的。 Spring Boot 是由 Pivotal 團隊提供的全新 Web 框架, 它主要的特點...
摘要:對請求的目標進行限流例如某個每分鐘只允許調用多少次對客戶端的訪問進行限流例如某個每分鐘只允許請求多少次對某些特定用戶或者用戶組進行限流例如非用戶限制每分鐘只允許調用次某個等多維度混合的限流。 對請求的目標URL進行限流(例如:某個URL每分鐘只允許調用多少次) 對客戶端的訪問IP進行限流(例如:某個IP每分鐘只允許請求多少次) 對某些特定用戶或者用戶組進行限流(例如:非VIP用戶限制...
摘要:上篇文章緩存機制介紹了的緩存機制,相信大家對有了進一步的了解,本文將詳細介紹網關如何實現服務下線的實時感知。目前網關實現的是對網關下游服務的實時感知,而且需滿足以下條件生產者需部署在容器管理平臺生產者做正常的下線升級或者縮容操作。 上篇文章《Eureka 緩存機制》介紹了Eureka的緩存機制,相信大家對Eureka 有了進一步的了解,本文將詳細介紹API網關如何實現服務下線的實時感知...
閱讀 1411·2021-10-08 10:04
閱讀 733·2021-09-07 09:58
閱讀 2912·2019-08-30 15:55
閱讀 2424·2019-08-29 17:21
閱讀 2126·2019-08-28 18:04
閱讀 3075·2019-08-28 17:57
閱讀 715·2019-08-26 11:46
閱讀 2228·2019-08-23 17:20