这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。
权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库的,也有很多用 JWT(JSON Web Token)的。
说实话这方面我的经验不多,又着急赶项目,所以就先用个简单的方案。
登录成功后将 Token 返回给前端,同时将 Token 存在 Redis 里。每次请求接口都从 Cookie 或 Header 中取出 Token,在从 Redis 中取出存储的 Token,比对是否一致。
我知道这方案不是最完美的,还有安全性问题,容易被劫持。但目前的策略是先把项目功能做完,上线之后再慢慢优化,不在一个功能点上扣的太细,保证项目进度不至于太慢。
项目地址:https://github.com/cachecats/coderiver
本文将分四部分介绍
- 登录逻辑
- AuthFilter 前置过滤器校验逻辑
- 工具类
- 演示验证
一、登录逻辑
登录成功后,将生成的 Token 存储在 Redis 中。用 String 类型的 key, value 格式存储,key是 TOKEN_userId
,如果用户的 userId 是 222222
,那键就是 TOKEN_222222
;值是生成的 Token。
只贴出登录的 Serive 代码
代码语言:javascript复制@Override
public UserInfoDTO loginByEmail(String email, String password) {
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UserException(ResultEnum.EMAIL_PASSWORD_EMPTY);
}
UserInfo user = userRepository.findUserInfoByEmail(email);
<span class="hljs-keyword">if</span> (user == <span class="hljs-keyword">null</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UserException(ResultEnum.EMAIL_NOT_EXIST);
}
<span class="hljs-keyword">if</span> (!user.getPassword().equals(password)) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UserException(ResultEnum.PASSWORD_ERROR);
}
<span class="hljs-comment">//生成 token 并保存在 Redis 中</span>
String token = KeyUtils.genUniqueKey();
<span class="hljs-comment">//将token存储在 Redis 中。键是 TOKEN_用户id, 值是token</span>
redisUtils.setString(String.format(RedisConsts.TOKEN_TEMPLATE, user.getId()), token, <span class="hljs-number">2l</span>, TimeUnit.HOURS);
UserInfoDTO dto = <span class="hljs-keyword">new</span> UserInfoDTO();
BeanUtils.copyProperties(user, dto);
dto.setToken(token);
<span class="hljs-keyword">return</span> 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 中取。
代码语言:javascript复制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 {
<span class="hljs-meta">@Autowired</span>
StringRedisTemplate stringRedisTemplate;
<span class="hljs-comment">//排除过滤的 uri 地址</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String LOGIN_URI = <span class="hljs-string">"/user/user/login"</span>;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String REGISTER_URI = <span class="hljs-string">"/user/user/register"</span>;
<span class="hljs-comment">//无权限时的提示语</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String INVALID_TOKEN = <span class="hljs-string">"invalid token"</span>;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String INVALID_USERID = <span class="hljs-string">"invalid userId"</span>;
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">filterType</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> PRE_TYPE;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">filterOrder</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> PRE_DECORATION_FILTER_ORDER - <span class="hljs-number">1</span>;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldFilter</span><span class="hljs-params">()</span> </span>{
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
log.info(<span class="hljs-string">"uri:{}"</span>, request.getRequestURI());
<span class="hljs-comment">//注册和登录接口不拦截,其他接口都要拦截校验 token</span>
<span class="hljs-keyword">if</span> (LOGIN_URI.equals(request.getRequestURI()) ||
REGISTER_URI.equals(request.getRequestURI())) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">run</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> ZuulException </span>{
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
<span class="hljs-comment">//先从 cookie 中取 token,cookie 中取失败再从 header 中取,两重校验</span>
<span class="hljs-comment">//通过工具类从 Cookie 中取出 token</span>
Cookie tokenCookie = CookieUtils.getCookieByName(request, <span class="hljs-string">"token"</span>);
<span class="hljs-keyword">if</span> (tokenCookie == <span class="hljs-keyword">null</span> || StringUtils.isEmpty(tokenCookie.getValue())) {
readTokenFromHeader(requestContext, request);
} <span class="hljs-keyword">else</span> {
verifyToken(requestContext, request, tokenCookie.getValue());
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
<span class="hljs-comment">/**
* 从 header 中读取 token 并校验
*/</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readTokenFromHeader</span><span class="hljs-params">(RequestContext requestContext, HttpServletRequest request)</span> </span>{
<span class="hljs-comment">//从 header 中读取</span>
String headerToken = request.getHeader(<span class="hljs-string">"token"</span>);
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(headerToken)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
} <span class="hljs-keyword">else</span> {
verifyToken(requestContext, request, headerToken);
}
}
<span class="hljs-comment">/**
* 从Redis中校验token
*/</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">verifyToken</span><span class="hljs-params">(RequestContext requestContext, HttpServletRequest request, String token)</span> </span>{
<span class="hljs-comment">//需要从cookie或header 中取出 userId 来校验 token 的有效性,因为每个用户对应一个token,在Redis中是以 TOKEN_userId 为键的</span>
Cookie userIdCookie = CookieUtils.getCookieByName(request, <span class="hljs-string">"userId"</span>);
<span class="hljs-keyword">if</span> (userIdCookie == <span class="hljs-keyword">null</span> || StringUtils.isEmpty(userIdCookie.getValue())) {
<span class="hljs-comment">//从header中取userId</span>
String userId = request.getHeader(<span class="hljs-string">"userId"</span>);
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(userId)) {
setUnauthorizedResponse(requestContext, INVALID_USERID);
} <span class="hljs-keyword">else</span> {
String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userId));
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
}
}
} <span class="hljs-keyword">else</span> {
String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userIdCookie.getValue()));
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
}
}
}
<span class="hljs-comment">/**
* 设置 401 无权限状态
*/</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setUnauthorizedResponse</span><span class="hljs-params">(RequestContext requestContext, String msg)</span> </span>{
requestContext.setSendZuulResponse(<span class="hljs-keyword">false</span>);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
ResultVO vo = <span class="hljs-keyword">new</span> ResultVO();
vo.setCode(<span class="hljs-number">401</span>);
vo.setMsg(msg);
Gson gson = <span class="hljs-keyword">new</span> Gson();
String result = gson.toJson(vo);
requestContext.setResponseBody(result);
}
}
三、工具类
MD5 工具类
代码语言:javascript复制package com.solo.coderiver.user.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
生成 MD5 的工具类
*/
public class MD5Utils {
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">getMd5</span><span class="hljs-params">(String plainText)</span> </span>{
<span class="hljs-keyword">try</span> {
MessageDigest md = MessageDigest.getInstance(<span class="hljs-string">"MD5"</span>);
md.update(plainText.getBytes());
<span class="hljs-keyword">byte</span> b[] = md.digest();
<span class="hljs-keyword">int</span> i;
StringBuffer buf = <span class="hljs-keyword">new</span> StringBuffer(<span class="hljs-string">""</span>);
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> offset = <span class="hljs-number">0</span>; offset < b.length; offset ) {
i = b[offset];
<span class="hljs-keyword">if</span> (i < <span class="hljs-number">0</span>)
i = <span class="hljs-number">256</span>;
<span class="hljs-keyword">if</span> (i < <span class="hljs-number">16</span>)
buf.append(<span class="hljs-string">"0"</span>);
buf.append(Integer.toHexString(i));
}
<span class="hljs-comment">//32位加密</span>
<span class="hljs-keyword">return</span> buf.toString();
<span class="hljs-comment">// 16位的加密</span>
<span class="hljs-comment">//return buf.toString().substring(8, 24);</span>
} <span class="hljs-keyword">catch</span> (NoSuchAlgorithmException e) {
e.printStackTrace();
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
}
<span class="hljs-comment">/**
* 加密解密算法 执行一次加密,两次解密
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">convertMD5</span><span class="hljs-params">(String inStr)</span></span>{
<span class="hljs-keyword">char</span>[] a = inStr.toCharArray();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < a.length; i ){
a[i] = (<span class="hljs-keyword">char</span>) (a[i] ^ <span class="hljs-string">'t'</span>);
}
String s = <span class="hljs-keyword">new</span> String(a);
<span class="hljs-keyword">return</span> s;
}
}
生成 key 的工具类
代码语言:javascript复制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/coderiver