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

本文主旨搭建一个无状态统一身份认证的系统,基于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次访问成功

在这里插入图片描述

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值