内容简介:这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库的,也有很多用 JWT(JSON Web Token)的。说实话这方面我的经验不多,又着急赶项目,所以就先用个简单的方案。
这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。
权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库的,也有很多用 JWT(JSON Web Token)的。
说实话这方面我的经验不多,又着急赶项目,所以就先用个简单的方案。
登录成功后将 Token 返回给前端,同时将 Token 存在 Redis 里。每次请求接口都从 Cookie 或 Header 中取出 Token,在从 Redis 中取出存储的 Token,比对是否一致。
我知道这方案不是最完美的,还有安全性问题,容易被劫持。但目前的策略是先把项目功能做完,上线之后再慢慢优化,不在一个功能点上扣的太细,保证项目进度不至于太慢。
项目地址: github.com/cachecats/c…
本文将分四部分介绍
- 登录逻辑
- 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 后端的全平台型全栈项目,欢迎关注。
项目地址: github.com/cachecats/c…
您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星:sparkles: ~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- vue实战 - 车牌号校验和银行校验
- 更加灵活的参数校验,Spring-boot自定义参数校验注解
- 一坨一坨的 if/else 参数校验,终于被 Spring Boot 参数校验组件整干净了
- SpringMVC——数据校验
- gin请求数据校验
- Dojo 表单校验
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Two Scoops of Django
Daniel Greenfeld、Audrey M. Roy / CreateSpace Independent Publishing Platform / 2013-4-16 / USD 29.95
Two Scoops of Django: Best Practices For Django 1.5 is chock-full of material that will help you with your Django projects. We'll introduce you to various tips, tricks, patterns, code snippets, and......一起来看看 《Two Scoops of Django》 这本书的介绍吧!