服务网关 Zuul 与 Redis 结合实现 Token 权限校验

2022-04-06 13:08:42 浏览数 (1)

这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。

权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库的,也有很多用 JWT(JSON Web Token)的。

说实话这方面我的经验不多,又着急赶项目,所以就先用个简单的方案。

登录成功后将 Token 返回给前端,同时将 Token 存在 Redis 里。每次请求接口都从 Cookie 或 Header 中取出 Token,在从 Redis 中取出存储的 Token,比对是否一致。

我知道这方案不是最完美的,还有安全性问题,容易被劫持。但目前的策略是先把项目功能做完,上线之后再慢慢优化,不在一个功能点上扣的太细,保证项目进度不至于太慢。

项目地址:https://github.com/cachecats/coderiver

本文将分四部分介绍

  1. 登录逻辑
  2. AuthFilter 前置过滤器校验逻辑
  3. 工具类
  4. 演示验证

一、登录逻辑

登录成功后,将生成的 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 &lt; b.length; offset  ) {
            i = b[offset];
            <span class="hljs-keyword">if</span> (i &lt; <span class="hljs-number">0</span>)
                i  = <span class="hljs-number">256</span>;
            <span class="hljs-keyword">if</span> (i &lt; <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 &lt; 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

0 人点赞