• Spring Gateway使用JWT实现统一身份认证


    在开发集群式或分布式服务时,鉴权是最重要的一步,为了方便对请求统一鉴权,一般都是会放在网关中进行处理。目前非常流行的一种方案是使用JWT,详细的使用说明,可以找相关的资料查阅,这里先不进行深入的引用了。主要使用它下面的特性:

    • 它的数据使用JSON格式封装。所以JWT是可以在不同的开发语音中传递。
    • 在payload可以加载部分业务数据,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
    • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
    • 它不需要在服务端保存会话信息, 减少了内存占用,也不需要落地存储,提升了检查效率。
    • JWT 使用的密钥都是在服务器端,不会暴露到客户端,所以是安全的。

    具体的流程如下:

    1. 用户先访问登陆授权服务器,授权验证通过之后,返回给客户端授权服务器生成的JWT Token字符串
    2. 客户端再访问后面的接口时,将授权服务器返回的JWT Token添加到header中
    3. 服务器网关收到客户端请求时,检测JWT Token是否合法,如果不合法,拒绝访问,返回错误。

    在这里插入图片描述
    需要处理的另一个问题是JWT Token 失败的问题,比如用户修改了密码,原来的JWT Token就不能再被使用了,一般是做法是添加JWT Token的黑名单,直到JWT Token失败。毕竟触发某些事件让JWT Token失效还是低概率事件。
    做法如下:

    • 当JWT Token失效事件发生时,将原来的JWT TOKEN 加入的黑名单中,黑名单,可以存到Redis或数据库中。
    • 为了提升处理效率,网关服务定时从授权服务刷新黑名单到网关服务内存中,这样检测JWT Token是否在黑名单中效率比较高
    • 在黑名单中的JWT Token 过期后,自动从黑名单中删除,防止黑名单数量堆积。
    • 为了防止用户JWT Token扩展,用户登陆之后检测,如果已存在JWT Token 且过期时间大于1天,就返回旧的JWT Token,否则自动延期,返回新的JWT Token

    实现方式

    • 在项目pom.xml中添加依赖
     		
                io.jsonwebtoken
                jjwt
                0.9.1
            
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 创建JWT Token的管理类
    package com.xinyue.game.jwt;
    
    import java.time.Duration;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    import com.alibaba.fastjson.JSON;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.CompressionCodecs;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.Header;
    import io.jsonwebtoken.Jwt;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    
    /**
     * @author 王广帅
     * @since 2023/5/23 22:27
     **/
    public class GameJwtService {
    
        private final static String JWT_SUBJECT = "game_token";
        private final static String TOKEN_EXTRA_KEY = "token_extra_key";
    
        /**
         * 创建一个Jwt token
         *
         * @param data 需要携带的业务数据,这里不要放置敏感信息,因为它是明文传输的
         * @param key  HS512的签名密钥
         * @return
         */
        public String createJwtToken(Object data, Duration expire, byte[] key) {
            Date expireDate = new Date(System.currentTimeMillis() + expire.toMillis());
            JwtBuilder jwtBuilder = Jwts.builder().setSubject(JWT_SUBJECT);
            if (data != null) {
                Map claims = new HashMap<>();
                claims.put(TOKEN_EXTRA_KEY, JSON.toJSONString(data));
                jwtBuilder.addClaims(claims);
            }
            String token = jwtBuilder.setExpiration(expireDate).compressWith(CompressionCodecs.DEFLATE).signWith(SignatureAlgorithm.HS512, key).compact();
            return token;
        }
    
        /**
         * 检查 jwt token并获取token携带的业务数据
         *
         * @param token
         * @param key
         * @return
         * @throws JwtTokenExpiredException
         * @throws JwtTokenErrorException
         */
        public  T checkTokenAndGet(String token, byte[] key, Class clazz) throws JwtTokenExpiredException, JwtTokenErrorException {
            try {
                Jws headerClaimsJwt = Jwts.parser().requireSubject(JWT_SUBJECT).setSigningKey(key).parseClaimsJws(token);
                Claims body = headerClaimsJwt.getBody();
                String value = (String) body.get(TOKEN_EXTRA_KEY);
                return JSON.parseObject(value, clazz);
            } catch (ExpiredJwtException e) {
                throw new JwtTokenExpiredException("token 已过期");
            } catch (Throwable e) {
                throw new JwtTokenErrorException("token不合法");
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 在Spring Cloud Gateway中添加全局过滤器
      使用全局过滤器,我们检测所有的请求是否合法,这里需要一个配置,因为有些请求是不需要检测token的,比如登陆和注册等,
    package com.xinyue.game.web.gateway.access;
    
    import java.nio.charset.StandardCharsets;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Service;
    import org.springframework.util.ObjectUtils;
    import org.springframework.web.server.ServerWebExchange;
    
    import com.alibaba.fastjson.JSONObject;
    import com.xinyue.game.jwt.GameJwtService;
    import com.xinyue.game.jwt.GameUserToken;
    import com.xinyue.game.jwt.JwtTokenErrorException;
    import com.xinyue.game.jwt.JwtTokenExpiredException;
    import com.xinyue.game.web.gateway.common.XinYueWebGatewaySystemConfig;
    
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    import reactor.netty.ByteBufFlux;
    
    /**
     * 访问授权过滤器,如果访问的地址,不在忽略名单内,则必须经过授权检测才可以访问
     *
     * @author 王广帅
     * @since 2023/5/23 21:12
     **/
    @Service
    public class AccessAuthVerifyFilter implements GlobalFilter {
    
        private Logger logger = LoggerFactory.getLogger(AccessAuthVerifyFilter.class);
        @Autowired
        private XinYueWebGatewaySystemConfig webGatewaySystemConfig;
    
    
        private GameJwtService gameJwtService = new GameJwtService();
    
    
        private boolean isIgnoreCheckUri(String uri) {
            return webGatewaySystemConfig.getUriAuthIgnoreList().contains(uri);
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String userToken = exchange.getRequest().getHeaders().getFirst("Authorization");
            // 获取请求的路径
            String uri = exchange.getRequest().getPath().value();
            logger.debug("收到请求:{}", uri);
            if (ObjectUtils.isEmpty(userToken)) {
                if (isIgnoreCheckUri(uri)) {
                    // 如果是不需要检测的请求,直接返回成功
                    return chain.filter(exchange);
                }else {
                    return this.responseError(exchange, 5001, "未登陆成功,请重新登陆之后再重试");
                }
            } else {
                byte[] key = webGatewaySystemConfig.getTokenAesKey().getBytes(StandardCharsets.UTF_8);
                try {
                    GameUserToken gameRoleToken = gameJwtService.checkTokenAndGet(userToken, key, GameUserToken.class);
                    if (gameRoleToken == null || gameRoleToken.getUserId() == null) {
                        return this.responseError(exchange, 5001, "登陆数据不正确,请重新登陆");
                    }
                } catch (JwtTokenExpiredException e) {
                    return this.responseError(exchange, 5002, "登陆已过期,请重新登陆");
                } catch (JwtTokenErrorException e) {
                    return this.responseError(exchange, 5003, "非法登陆,请重新登陆");
                }
            }
            // 如果没有异常,继续往下传递
            return chain.filter(exchange);
        }
    
        /**
         * 统一响应错误提示
         *
         * @param exchange
         * @param code
         * @param msg
         * @return
         */
        private Mono<Void> responseError(ServerWebExchange exchange, int code, String msg) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            JSONObject data = new JSONObject();
            data.put("code", code);
            data.put("msg", msg);
            byte[] dataBytes = data.toJSONString().getBytes(StandardCharsets.UTF_8);
            Mono<Void> ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(dataBytes))));
            return ret;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    实现源码地址:https://gitee.com/wgslucky/xinyue-game-frame

  • 相关阅读:
    《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(AlexNet)
    线程池工作原理及参数
    uniapp h5实现微信公众号登录
    webpack打包 - webpack篇
    14:00面试,14:08就出来了,问的问题有点变态了。。。
    Apollo6.0安装文档教程——环境搭建、安装、编译、测试
    【Python搜索算法】广度优先搜索(BFS)算法原理详解与应用,示例+代码
    ZZNUOJ_用Java编写程序实现1585:super prime(附源码)
    什么是ReFi?
    15uec++多人游戏【残血提示材质与控件】
  • 原文地址:https://blog.csdn.net/wgslucky/article/details/130835977