这两天在写项目的全局权限校验,用 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…
您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~