资讯专栏INFORMATION COLUMN

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

flyer_dev / 3573人阅读

摘要:项目地址本文将分四部分介绍登录逻辑前置过滤器校验逻辑工具类演示验证一登录逻辑登录成功后,将生成的存储在中。键是用户值是二前置过滤器继承自,必须实现的四个方法。

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

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

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

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

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

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

本文将分四部分介绍

登录逻辑

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后端的全平台型全栈项目,欢迎关注。

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

您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72270.html

相关文章

  • 你想要的全平台全栈开源项目-Vue、React、小程序、安卓、ReactNative、java后端

    摘要:无论你是前端后端移动端开发人员,或是设计师产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。 全平台全栈开源项目 coderiver 今天终于开始前后端联调了~ 首先感谢大家的支持,coderiver 在 GitHub 上开源两周,获得了 54 个 Star,9 个 Fork,5 个 Watch。 这些鼓励和认可也更加坚定了我继续写下去的决心~ 再次感谢各位大佬! ...

    Maxiye 评论0 收藏0
  • 深入理解Spring Cloud服务构建【二】 - 2.2 Spring Cloud

    摘要:负载均衡组件是一个负载均衡组件,它通常和配合使用。和配合,很容易做到负载均衡,将请求根据负载均衡策略分配到不同的服务实例中。和配合,在消费服务时能够做到负载均衡。在默认的情况下,和相结合,能够做到负载均衡智能路由。 2.2.1 简介 Spring Cloud 是基于 Spring Boot 的。 Spring Boot 是由 Pivotal 团队提供的全新 Web 框架, 它主要的特点...

    Rocko 评论0 收藏0
  • Zuul:构建高可用网关之多维度限流

    摘要:对请求的目标进行限流例如某个每分钟只允许调用多少次对客户端的访问进行限流例如某个每分钟只允许请求多少次对某些特定用户或者用户组进行限流例如非用户限制每分钟只允许调用次某个等多维度混合的限流。 对请求的目标URL进行限流(例如:某个URL每分钟只允许调用多少次) 对客户端的访问IP进行限流(例如:某个IP每分钟只允许请求多少次) 对某些特定用户或者用户组进行限流(例如:非VIP用户限制...

    wenshi11019 评论0 收藏0
  • API网关如何实现服务下线实时感知

    摘要:上篇文章缓存机制介绍了的缓存机制,相信大家对有了进一步的了解,本文将详细介绍网关如何实现服务下线的实时感知。目前网关实现的是对网关下游服务的实时感知,而且需满足以下条件生产者需部署在容器管理平台生产者做正常的下线升级或者缩容操作。 上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka 有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知...

    codeKK 评论0 收藏0

发表评论

0条评论

flyer_dev

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<