Spring cloud+Zuul+JWT实现无状态统一身份认证和分布式限流

2023-10-31

本文主旨搭建一个无状态统一身份认证的系统,基于Spring cloud微服务架构,Eureka 实现服务的注册与发现,Zuul网关实现服务路由,请求过滤和限流功能,使用JWT规范实现客户登陆信息的服务端无状态话,相关文章参考《Spring cloud架构解析和框架搭建》,《Web用户认证和授权机制的演进》

架构

在这里插入图片描述

搭建eureka服务注册中心,将业务服务和认证授权中心服务注册进去,zuul网关路由业务服务和认证中心服务,并实现请求过滤和限流功能,认证的流程如下:

1.客户端发起访问服务的请求,zuul过滤请求,如果请求不需要身份验证则放行路由到指定服务,如果请求需要身份验证则校验token,如果token校验失败拒绝请求
2.客户端登陆,带入用户名密码
3.Zuul路由到认证授权中心,校验用户名密码,通过后生成JWT并返回客户端
4.客户端本地保存JWT
5.客户端访问需要验证身份的服务,并带入JWT
6.Zuul校验JWT,成功后放行路由到该服务

搭建

创建JWT工具类

创建JWT的生成,解码方法

package com.iwc.cloudBoss.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT校验工具类
 * <ol>
 * <li>iss: jwt签发者</li>
 * <li>sub: jwt所面向的用户</li>
 * <li>aud: 接收jwt的一方</li>
 * <li>exp: jwt的过期时间,这个过期时间必须要大于签发时间</li>
 * <li>nbf: 定义在什么时间之前,该jwt都是不可用的</li>
 * <li>iat: jwt的签发时间</li>
 * <li>jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击</li>
 * </ol>
 */

public class JWTUtil {

    private final static Logger log= LoggerFactory.getLogger(JWTUtil.class);

    /**
     * JWT 加解密类型
     */
    private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256;
    /**
     * JWT 生成密钥使用的密码
     */
    private static final String SECRET = "iwc.cloudBoss";

    /**
     * JWT 添加至HTTP HEAD中的前缀
     */
    private static final String SEPARATOR = "Bearer ";

    /**
     * JWT 添加至PAYLOAD的签发者
     */
    private static final String ISSUE = "iwc";

    /**
     * JWT 添加至PAYLOAD的有效期(秒)
     */
    private static final int TIMEOUT = 60 * 60 * 24;

    /**
     * 生成JWT
     *
     * @param userId
     * @return
     */
    public static String genJWT(String userId) {

        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);

        long currentTime = System.currentTimeMillis();
        return Jwts.builder()
            .setId(UUID.randomUUID().toString()) //jwt唯一id
            .setIssuedAt(new Date(currentTime))  //签发时间
            .setIssuer(ISSUE) //签发者信息

            .signWith(ALGORITHM, SECRET) //加密方式
            .setExpiration(new Date(currentTime + TIMEOUT * 1000))  //过期时间戳
            .addClaims(claims) //cla信息
            .compact();
    }

    /**
     * 获取token中的claims信息
     *
     * @param token
     * @return
     */
    private static Jws<Claims> getJws(String token) {
        return Jwts.parser()
            .setSigningKey(SECRET)
            .parseClaimsJws(token);
    }

    public static String getSignature(String token) {
        try {
            return getJws(token).getSignature();
        } catch (Exception ex) {
            return "";
        }
    }

    /**
     * 获取token中head信息
     *
     * @param token
     * @return
     */
    public static JwsHeader getHeader(String token) {
        try {
            return getJws(token).getHeader();
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * 获取payload body信息
     *
     * @param token
     * @return
     */
    public static Claims getClaimsBody(String token) {
        return getJws(token).getBody();
    }

    /**
     * 获取body某个值
     *
     * @param token
     * @param key
     * @return
     */
    public static Object getVal(String token, String key) {
        return getJws(token).getBody().get(key);
    }

    /**
     * 是否过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        try {
            return getClaimsBody(token)
                .getExpiration()
                .before(new Date());
        } catch (ExpiredJwtException ex) {
            return true;
        }
    }
}

认证授权中心

依赖包

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

认证服务
在登陆校验成功后生成JWT并返回前端

/**
 * JWT方案
 */
String token = JWTUtil.genJWT(custMember.getId().toString());
Member member = DtoUtil.TbMemer2Member(custMember);
member.setToken(token);
member.setState(1);

return member;

网关

依赖包

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

配置路由规则

zuul:
  #忽略所有服务,只路由指定的服务
  ignored-services: "*"
  #ignoredPatterns: /**/cust/** 不允许转发的路径
  routes:
    cloudBoss-customerCenter: /**

身份验证

token过滤
继承ZuulFileter类,实现过滤类型,过滤优先级,是否过滤,和执行过滤四个方法
在是否过滤方法中可以通过URL判断是否需要过滤,在执行过滤方法中获得HTTP请求头中的token,并校验

@Component
public class TokenFilter extends ZuulFilter {

    private final static Logger log= LoggerFactory.getLogger(TokenFilter.class);

    /**
     四种类型:pre,routing,error,post
     pre:主要用在路由映射的阶段是寻找路由映射表的
     routing:具体的路由转发过滤器是在routing路由器,具体的请求转发的时候会调用
     error:一旦前面的过滤器出错了,会调用error过滤器。
     post:当routing,error运行完后才会调用该过滤器,是在最后阶段的
     */
    @Override public String filterType() {
        return "pre";
    }

    /**
     * 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行
    */
    @Override public int filterOrder() {
        return 0;
    }

    /**
     * 控制过滤器生效不生效,可以在里面写一串逻辑来控制
     */
    @Override public boolean shouldFilter() {
        //共享RequestContext,上下文对象
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //不需要权限校验URL
        if ("/cust/login/login".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/checkLogin".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/loginOut".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/register".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        }
/*        else if ("/cust/register/getPreFix".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } */
        else if ("/member/sendSmsCode".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/checkUser".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/forgetPassword".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/signUp".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/member/payOrderForPG".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/error".equalsIgnoreCase(request.getRequestURI())) {
            return false;
        } else if ("/goods/".equalsIgnoreCase(request.getRequestURI().substring(0, 7))) {
            return false;
        }
        return true;
    }

    /**
     * 执行过滤逻辑
     * 只有上面返回true的时候,才会进入到该方法
     */
    @Override public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
         /**JWT方案
         *请求头获取JWT
         */
        //如果不存在token拒绝请求
        String token = request.getHeader("Authentication-Token");
        if (StringUtils.isEmpty(token)) {
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            context.setResponseBody("unAuthrized no token");
            return null;
        }

        //jwt验证,获取userId
        try {
            String userId = JWTUtil.getVal(token, "userId").toString();
            //token值有问题拒绝请求
            if (StringUtils.isEmpty(userId)) {
                context.setSendZuulResponse(false);
                context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
                context.setResponseBody("token auth fail");
                return null;
            }
            //token过期拒绝请求
            else if (JWTUtil.isExpiration(token)) {
                context.setSendZuulResponse(false);
                context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
                context.setResponseBody("token expired");
                return null;
            }
        }
        catch (Exception e) {
            log.error("token auth fail");
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            context.setResponseBody("token auth fail");
        }
        return null;
    }
}

限流

Redis存储限流令牌

配置jedis pool

@Configuration public class ConfigJedisPool {
    @Bean(name = "jedis.pool") @Autowired
    public JedisPool jedisPool(@Value("${spring.redis.host}") String host, @Value("${spring.redis.port}") int port) {
        return new JedisPool(host, port);
    }
}

Redis实现令牌桶机制

@Component
public class RedisRaterLimiter {

    final static Logger log= LoggerFactory.getLogger(RedisRaterLimiter.class);

    @Autowired
    private JedisPool jedisPool;

    private static final String BUCKET = "BUCKET_";
    private static final String BUCKET_COUNT = "BUCKET_COUNT";
    private static final String BUCKET_MONITOR = "BUCKET_MONITOR_";

    public String acquireTokenFromBucket(String point, int limit, long timeout) {

        Jedis jedis = jedisPool.getResource();
        try{
            //UUID令牌
            String token = UUID.randomUUID().toString();
            long now = System.currentTimeMillis();
            //开启事务
            Transaction transaction = jedis.multi();

            //删除信号量 移除有序集中指定区间(score)内的所有成员 ZREMRANGEBYSCORE key min max
            transaction.zremrangeByScore((BUCKET_MONITOR + point).getBytes(), "-inf".getBytes(), String.valueOf(now - timeout).getBytes());
            //为每个有序集分别指定一个乘法因子(默认设置为 1) 每个成员的score值在传递给聚合函数之前都要先乘以该因子
            ZParams params = new ZParams();
            params.weightsByDouble(1.0, 0.0);
            //计算给定的一个或多个有序集的交集
            transaction.zinterstore(BUCKET + point, params, BUCKET + point, BUCKET_MONITOR + point);

            //计数器自增
            transaction.incr(BUCKET_COUNT);
            List<Object> results = transaction.exec();
            long counter = (Long) results.get(results.size() - 1);

            transaction = jedis.multi();
            //Zadd 将一个或多个成员元素及其分数值(score)加入到有序集当中
            transaction.zadd(BUCKET_MONITOR + point, now, token);
            transaction.zadd(BUCKET + point, counter, token);
            transaction.zrank(BUCKET + point, token);
            results = transaction.exec();
            //获取排名,判断请求是否取得了信号量
            long rank = (Long) results.get(results.size() - 1);
            if (rank < limit) {
                return token;
            } else {
                //没有获取到信号量,清理之前放入redis中垃圾数据
                transaction = jedis.multi();
                //Zrem移除有序集中的一个或多个成员
                transaction.zrem(BUCKET_MONITOR + point, token);
                transaction.zrem(BUCKET + point, token);
                transaction.exec();
            }
        }catch (Exception e){
            log.error("限流出错"+e.toString());
        }finally {
            if(jedis!=null){
                jedis.close();
            }
        }
        return null;
    }

限流过滤
在执行过滤逻辑中访问redis令牌库

/**
 * 执行过滤逻辑
 * 只有上面返回true的时候,才会进入到该方法
 */
@Override public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();

    if(rateLimitEnable){
        String token1 = redisRaterLimiter.acquireTokenFromBucket("cloudBoss"+ IPInfoUtil.getIpAddr(request), ipLimit, ipTimeout * 1000);
        if (StringUtils.isBlank(token1)) {
            throw new CloudBossException("You have too many current requests, please try again later");
        }

        String token2 = redisRaterLimiter.acquireTokenFromBucket("cloudBoss_All", limit, timeout * 1000);
        if (StringUtils.isBlank(token2)) {
            throw new CloudBossException("There are too many current requests, please try again later");
        }
    }

    return null;
}

测试

身份验证

请求需要验证身份的服务
Post测试调用zuul的地址http://localhost:9881/cust/register/getPreFix
返回无token
在这里插入图片描述

登陆

请求登陆服务,zuul放行,登陆成功

在这里插入图片描述

并返回JWT串

在这里插入图片描述
这里我们手动复制该串在后面的请求中带入

再次请求第一个服务
将复制的JWT串带入HTTP头
在这里插入图片描述
访问通过

限流

Zuul的限流配置

#启用全局限流
cloudBoss.rateLimit.enable: true
#每n秒内
cloudBoss.rateLimit.timeout: 10
#限制n个请求
cloudBoss.rateLimit.limit: 100
#单个ip限制
cloudBoss.rateLimit.perIP.timeout: 10
#限制n个请求
cloudBoss.rateLimit.perIP.limit: 5

验证单个ip限流10秒钟内5次

JMeter压测不限流服务
访问http://localhost:9881/cust/register/getPreFix
十秒钟访问20次
在这里插入图片描述

测试结果全部通过,没有限流

在这里插入图片描述

JMeter压测限流服务
访问http://localhost:9881/cust/login/login
十秒钟访问20次
在这里插入图片描述
成功限流10秒钟内5次访问成功

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring cloud+Zuul+JWT实现无状态统一身份认证和分布式限流 的相关文章

随机推荐

  • css补充2:flex布局,居中方案等

    一 flex 布局 1 1 flex布局原理 flex是flexible Box的缩写 意为 弹性布局 用来为盒状模型提供最大的灵活性 任何一个容器都可以指定为flex布局 当我们为父盒子设为flex布局以后 子元素的float clear
  • 相似度计算

    在推荐系统中 最基础的一个概念就是计算相似度 很多的相似度都是基于距离计算出来的 计算距离的方法有很多种 包括 Euclidian Distance Pearson Correlation Cosine Similarity interse
  • 【华为OD机试真题2023B卷 JAVA&JS】模拟消息队列

    华为OD2023 B卷 机试题库全覆盖 刷题指南点这里 模拟消息队列 知识点排序 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 让我们来模拟一个消息队列的运作 有一个发布者和若干消费者 发布者会在给定的时刻向消息队列发送消
  • JQuery美化下拉框插件

  • 简单卸载硬盘安装的ubuntu

    我们多数要卸载ubuntu的都是双系统 简简单单 两步搞定ubuntu的卸载前提拥有一张windows的光盘 上面带有PE系统即可 用优盘启动盘估计也可以第一步 先去除grub 进入PE 多数PE里面有一个分区表医生或者类似的软件 我用的F
  • ubuntu解压zip文件命令_Ubuntu压缩及解压文件简介

    使用Ubuntu的过程中 无论用来干什么 都会有文件上的交流 必不可免的就是压缩文件 Ubuntu系统中自带了部分格式的压缩软件 但是win系统习惯的rar格式文件解压需要下载相关软件 现整理如下 1 文件格式及解压工具 tar 用 tar
  • ARM中MMU之地址转换

    第一次发帖略微有点激动 有不足的地方还请各位大神指教 最近看了看mmu 找了好多资料 看了好多博文终于稍微清楚了一点 现在我把自己理解的内容总结一下 首先我来说一下MMU的作用 MMU就是负责虚拟地址 virtual address 转化成
  • response.getWriter().write()和 response.getWriter().print()的区别 以及 PrintWriter对象 和 out对象 的区别

    一 response getWriter write 和 response getWriter print 的区别 response getWriter 返回的是PrintWriter 这是一个打印输出流 response getWrite
  • Pytorch之经典神经网络Generative Model(三) —— GAN (MNIST)

    2014年由GAN之父Ian Goodfellow提出 加拿大蒙特利尔大学 GAN 生成式对抗网络 前面我们讲了自动编码器和变分自动编码器 不管是哪一个 都是通过计算生成图像和输入图像在每个像素点的误差来生成 loss 这一点是特别不好的
  • 一篇文章,让你彻底搞懂单例设计模式

    今天在一群小哥哥的带领下 本程序媛终于学会了所有单例模式 非常感谢大哥哥 小哥哥 下文是我总结的单例模式的八种实现方式 如下所示 单例模式的简介 我们将一个类在当前进程中只有一个实例的这种模式 称之为 单例模式 那么Java代码如何实现一个
  • Postman —— 配置环境变量

    PostMan是一套比较方便的接口测试工具 但我们在使用过程中 可能会出现创建了API请求 但API的URL会随着服务器IP地址的变化而改变 这样的情况下 如果每一个API都重新修改URL的话那将是非常的麻烦 所以PostMan中也提供环境
  • 【ViT 微调时关于position embedding如何插值(interpolate)的详解】

    目录 1 问题描述 2 positional embedding如何interpolate 3 输入的sequence length改变了ViT还能正常前向推断 本文适合对Vision Transformer有一定了解 知道内部结构和一些实
  • 项目6—利用中断实现单位数码管0~9显示

    项目5中有介绍中断问题 中断函数命名格式 函数值类型 函数名 形式参数列表 interrupt x interrupt为中断函数关键字 表中第二行T0中断 使能T0中断 就要将ET0置1 当它的中断标志位TF0变为1时 就会触发T0中断 这
  • UVA1613 K-GraphOddity

    UVA1613 K GraphOddity 题目传送门 刚看第一眼一点思路都没有 后面看了大佬的题解发现这道题其实是一道水题 用到的方法就是DFS遍历图 我是废物 题目意思很简单 就不分析了 下面直接说方法 首先求出k 然后dfs遍历一遍图
  • 出现这个问题 -bash: /etc/profile.d/env.sh: Permission denied

    记录一下搞了一个上午都没有解决的问题 如下图 出现的问题很奇怪 从root用户切换到普通用户是出现了 bash etc profile d env sh Permission denied 然后用root 把这个env sh文件 内容是PS
  • IP核之FIFO实验

    FIFO 的英文全称是 First In First Out 即先进先出 FPGA 使用的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器 常被用于数据的缓存 或者高速异步数据的交互也即所谓的跨时钟域信号传递 它与 FPGA
  • Python配置清华镜像源

    Python配置清华镜像源 1 前言 使用pip 安装服务器在国外的python 库时 下载需要很长时间 在配置文件中设置国内镜像可以提高速度 清华镜像源就是其中之一 2 pypi 镜像使用帮助 网址 https mirrors tuna
  • 将cookie字符串转化为json对象

    先看一下cookie长啥样 在控制台输入查看document cookie 如下图 分析可以看出cookie是以 key value key2 value2 的结构 好 接下来我们就将其转为json对象 function cookieToJ
  • base64 加密解密

    1 str lt gt bytes str bytes bytes s encoding utf8 bytes str str b encoding utf 8 此外还可通过编码解码的形式对二者进行转换 str 编码成 bytes 格式 s
  • Spring cloud+Zuul+JWT实现无状态统一身份认证和分布式限流

    本文主旨搭建一个无状态统一身份认证的系统 基于Spring cloud微服务架构 Eureka 实现服务的注册与发现 Zuul网关实现服务路由 请求过滤和限流功能 使用JWT规范实现客户登陆信息的服务端无状态话 相关文章参考 Spring