摘要:項(xiàng)目地址本文將分四部分介紹登錄邏輯前置過濾器校驗(yàn)邏輯工具類演示驗(yàn)證一登錄邏輯登錄成功后,將生成的存儲(chǔ)在中。鍵是用戶值是二前置過濾器繼承自,必須實(shí)現(xiàn)的四個(gè)方法。
這兩天在寫項(xiàng)目的全局權(quán)限校驗(yàn),用 Zuul 作為服務(wù)網(wǎng)關(guān),在 Zuul 的前置過濾器里做的校驗(yàn)。
權(quán)限校驗(yàn)或者身份驗(yàn)證就不得不提 Token,目前 Token 的驗(yàn)證方式有很多種,有生成 Token 后將 Token 存儲(chǔ)在 Redis 或數(shù)據(jù)庫的,也有很多用 JWT(JSON Web Token)的。
說實(shí)話這方面我的經(jīng)驗(yàn)不多,又著急趕項(xiàng)目,所以就先用個(gè)簡單的方案。
登錄成功后將 Token 返回給前端,同時(shí)將 Token 存在 Redis 里。每次請求接口都從 Cookie 或 Header 中取出 Token,在從 Redis 中取出存儲(chǔ)的 Token,比對是否一致。
我知道這方案不是最完美的,還有安全性問題,容易被劫持。但目前的策略是先把項(xiàng)目功能做完,上線之后再慢慢優(yōu)化,不在一個(gè)功能點(diǎn)上扣的太細(xì),保證項(xiàng)目進(jìn)度不至于太慢。
項(xiàng)目地址:https://github.com/cachecats/...
本文將分四部分介紹
登錄邏輯
AuthFilter 前置過濾器校驗(yàn)邏輯
工具類
演示驗(yàn)證
一、登錄邏輯登錄成功后,將生成的 Token 存儲(chǔ)在 Redis 中。用 String 類型的 key, value 格式存儲(chǔ),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存儲(chǔ)在 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,必須實(shí)現(xiàn) ZuulFilter 的四個(gè)方法。
filterType(): Filter 的類型,前置過濾器返回 PRE_TYPEfilterOrder(): Filter 的順序,值越小越先執(zhí)行。這里的寫法是 PRE_DECORATION_FILTER_ORDER - 1, 也是官方建議的寫法。
shouldFilter(): 是否應(yīng)該過濾。返回 true 表示過濾,false 不過濾??梢栽谶@個(gè)方法里判斷哪些接口不需要過濾,本例排除了注冊和登錄接口,除了這兩個(gè)接口,其他的都需要過濾。
run(): 過濾器的具體邏輯
為了方便前端,考慮到要給 pc、app、小程序等不同平臺(tái)提供服務(wù),token 設(shè)置在 cookie 和 header 任選一均可,會(huì)先從 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; /** * 權(quán)限驗(yàn)證 Filter * 注冊和登錄接口不過濾 * * 驗(yàn)證權(quán)限需要前端在 Cookie 或 Header 中(二選一即可)設(shè)置用戶的 userId 和 token * 因?yàn)?token 是存在 Redis 中的,Redis 的鍵由 userId 構(gòu)成,值是 token * 在兩個(gè)地方都沒有找打 userId 或 token其中之一,就會(huì)返回 401 無權(quán)限,并給與文字提示 */ @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"; //無權(quán)限時(shí)的提示語 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()); //注冊和登錄接口不攔截,其他接口都要攔截校驗(yàn) 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 中取,兩重校驗(yàn) //通過工具類從 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 并校驗(yàn) */ 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中校驗(yàn)token */ private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) { //需要從cookie或header 中取出 userId 來校驗(yàn) token 的有效性,因?yàn)槊總€(gè)用戶對應(yīng)一個(gè)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); } } } /** * 設(shè)置 401 無權(quán)限狀態(tài) */ 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; } } /** * 加密解密算法 執(zhí)行一次加密,兩次解密 */ 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 { /** * 產(chǎn)生獨(dú)一無二的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); } }四、演示驗(yàn)證
在 8084 端口啟動(dòng) api_gateway 項(xiàng)目,同時(shí)啟動(dòng) user 項(xiàng)目。
用 postman 通過網(wǎng)關(guān)訪問登錄接口,因?yàn)檫^濾器對登錄和注冊接口排除了,所以不會(huì)校驗(yàn)這兩個(gè)接口的 token。
可以看到,訪問地址 http://localhost:8084/user/user/login 登錄成功并返回了用戶信息和 token。
此時(shí)應(yīng)該把 token 存入 Redis 中了,用戶的 id 是 111111 ,所以鍵是 TOKEN_111111,值是剛生成的 token 值
再來隨便請求一個(gè)其他的接口,應(yīng)該走過濾器。
header 中不傳 token 和 userId,返回 401
只傳 token 不傳 userId,返回401并提示 invalid userId
token 和 userId 都傳,但 token 不對,返回401,并提示 invalid token
同時(shí)傳正確的 token 和 userId,請求成功
以上就是簡單的 Token 校驗(yàn),如果有更好的方案歡迎在評論區(qū)交流
代碼出自開源項(xiàng)目 CodeRiver,致力于打造全平臺(tái)型全棧精品開源項(xiàng)目。
coderiver 中文名 河碼,是一個(gè)為程序員和設(shè)計(jì)師提供項(xiàng)目協(xié)作的平臺(tái)。無論你是前端、后端、移動(dòng)端開發(fā)人員,或是設(shè)計(jì)師、產(chǎn)品經(jīng)理,都可以在平臺(tái)上發(fā)布項(xiàng)目,與志同道合的小伙伴一起協(xié)作完成項(xiàng)目。
coderiver河碼 類似程序員客棧,但主要目的是方便各細(xì)分領(lǐng)域人才之間技術(shù)交流,共同成長,多人協(xié)作完成項(xiàng)目。暫不涉及金錢交易。
計(jì)劃做成包含 pc端(Vue、React)、移動(dòng)H5(Vue、React)、ReactNative混合開發(fā)、Android原生、微信小程序、java后端的全平臺(tái)型全棧項(xiàng)目,歡迎關(guān)注。
項(xiàng)目地址:https://github.com/cachecats/...
您的鼓勵(lì)是我前行最大的動(dòng)力,歡迎點(diǎn)贊,歡迎送小星星? ~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/72270.html
摘要:無論你是前端后端移動(dòng)端開發(fā)人員,或是設(shè)計(jì)師產(chǎn)品經(jīng)理,都可以在平臺(tái)上發(fā)布項(xiàng)目,與志同道合的小伙伴一起協(xié)作完成項(xiàng)目。 全平臺(tái)全棧開源項(xiàng)目 coderiver 今天終于開始前后端聯(lián)調(diào)了~ 首先感謝大家的支持,coderiver 在 GitHub 上開源兩周,獲得了 54 個(gè) Star,9 個(gè) Fork,5 個(gè) Watch。 這些鼓勵(lì)和認(rèn)可也更加堅(jiān)定了我繼續(xù)寫下去的決心~ 再次感謝各位大佬! ...
摘要:負(fù)載均衡組件是一個(gè)負(fù)載均衡組件,它通常和配合使用。和配合,很容易做到負(fù)載均衡,將請求根據(jù)負(fù)載均衡策略分配到不同的服務(wù)實(shí)例中。和配合,在消費(fèi)服務(wù)時(shí)能夠做到負(fù)載均衡。在默認(rèn)的情況下,和相結(jié)合,能夠做到負(fù)載均衡智能路由。 2.2.1 簡介 Spring Cloud 是基于 Spring Boot 的。 Spring Boot 是由 Pivotal 團(tuán)隊(duì)提供的全新 Web 框架, 它主要的特點(diǎn)...
摘要:對請求的目標(biāo)進(jìn)行限流例如某個(gè)每分鐘只允許調(diào)用多少次對客戶端的訪問進(jìn)行限流例如某個(gè)每分鐘只允許請求多少次對某些特定用戶或者用戶組進(jìn)行限流例如非用戶限制每分鐘只允許調(diào)用次某個(gè)等多維度混合的限流。 對請求的目標(biāo)URL進(jìn)行限流(例如:某個(gè)URL每分鐘只允許調(diào)用多少次) 對客戶端的訪問IP進(jìn)行限流(例如:某個(gè)IP每分鐘只允許請求多少次) 對某些特定用戶或者用戶組進(jìn)行限流(例如:非VIP用戶限制...
摘要:上篇文章緩存機(jī)制介紹了的緩存機(jī)制,相信大家對有了進(jìn)一步的了解,本文將詳細(xì)介紹網(wǎng)關(guān)如何實(shí)現(xiàn)服務(wù)下線的實(shí)時(shí)感知。目前網(wǎng)關(guān)實(shí)現(xiàn)的是對網(wǎng)關(guān)下游服務(wù)的實(shí)時(shí)感知,而且需滿足以下條件生產(chǎn)者需部署在容器管理平臺(tái)生產(chǎn)者做正常的下線升級(jí)或者縮容操作。 上篇文章《Eureka 緩存機(jī)制》介紹了Eureka的緩存機(jī)制,相信大家對Eureka 有了進(jìn)一步的了解,本文將詳細(xì)介紹API網(wǎng)關(guān)如何實(shí)現(xiàn)服務(wù)下線的實(shí)時(shí)感知...
閱讀 1480·2021-10-08 10:04
閱讀 800·2021-09-07 09:58
閱讀 2980·2019-08-30 15:55
閱讀 2535·2019-08-29 17:21
閱讀 2240·2019-08-28 18:04
閱讀 3135·2019-08-28 17:57
閱讀 791·2019-08-26 11:46
閱讀 2344·2019-08-23 17:20