• SpringCloudAlibaba2.2.6.RELEASE集成Gateway并实现JWT鉴权


    • 搭建前提

    本文基于Nacos作为注册中心,进行搭建,所以前提是已经搭建了Nacos

    Nacos单机版搭建教程
    Nacos集群版搭建教程

    1.导入依赖

    注意gateway的pom文件不要引用MVC的依赖包,不然会报错。

      
            
            
                org.springframework.cloud
                spring-cloud-starter-gateway
            
    
            
            
                org.springframework.cloud
                spring-cloud-starter-openfeign
            
    
            
            
                com.alibaba.cloud
                spring-cloud-starter-alibaba-nacos-discovery
            
    
            
            
                com.wf
                api-commons
                ${project.version}
            
    
            
            
                org.projectlombok
                lombok
                true
            
        
    
    • 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

    2.编写启动类

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    
    /**
     * @Description: 网关微服务
     */
    @EnableDiscoveryClient
    @SpringBootApplication
    @Slf4j
    public class GatewayServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(GatewayServiceApplication.class, args);
            log.info("网关微服务启动成功");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.编写配置文件

    server:
      port: 9005
    spring:
      application:
        name: gateway-service
      cloud:
        #nacos相关配置-------------------------------------------------
        nacos:
          discovery:
            #配置Nacos服务注册地址
            server-addr: 192.168.31.78:8848
        gateway:
          discovery:
            locator:
              #开启注册中心路由功能
              enabled: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4.最终项目结构
    在这里插入图片描述

    • 测试注册中心路由功能
      1.启动全部微服务
      在这里插入图片描述
      2.不通过Gateway访问9004微服务
      请求:http://localhost:9004/test
      在这里插入图片描述
      返回结果:
      在这里插入图片描述

    3.通过Gateway访问9004微服务
    请求:http://localhost:9005/message-services/test

    注:本次请求携带了9004微服务的服务名称且端口号已经换成了9005
    在这里插入图片描述

    返回结果:
    在这里插入图片描述

    • 自定义路由配置
      1.新增yml文件内容
    server:
      port: 9005
    spring:
      application:
        name: gateway-service
      cloud:
        #nacos相关配置-------------------------------------------------
        nacos:
          discovery:
            #配置Nacos服务注册地址
            server-addr: 192.168.31.78:8848
        gateway:
          discovery:
            locator:
              #开启注册中心路由功能
              enabled: true
          routes:
           #该组配置的一个id值,需要保证他的唯一,可以设置为和服务名一致
            - id: message-services
             #通过条件匹配之后需要路由到的新的服务地址,lb:// 表示开启负载均衡策略去路由 
              uri: lb://message-services
               #url的匹配条件,及断言,规则为url中必须携带 /user/management才能进行转发
              predicates:
                - Path=/message/board/**
              #在路由前对请求的地址进行额外的其他操作,例如拼接或者裁减等。 此处的作用在于去除掉 predicates中配置的路径,因为该路径只是断言,并无实际地址
              filters:
               #表示删除第二个路径,即删除predicates中配置的/message/board
                - StripPrefix=2
    
    
    • 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

    2.再次使用上文中的测试地址测试
    请求:http://localhost:9005/message-services/test
    在这里插入图片描述
    返回结果:
    在这里插入图片描述
    提示404访问不到资源,因为此处使用了自定义路由,那么就应该携带自定义的请求路径

    3.使用自定义的请求路由访问
    请求:http://localhost:9005/message/board/test

    注:此处使用的/message/board在9004微服务中并不存在该资源,此处只是在gateway中yml文件中predicates配置的自定义路由,然后通过filters中的配置去除掉/message/board得到9004微服务的真实资源地址
    在这里插入图片描述
    返回结果:
    在这里插入图片描述

    解析请求过程:

    在这里插入图片描述

    • JWT统一鉴权
      1.配置过滤器
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.crypto.SecureUtil;
    import cn.hutool.crypto.symmetric.SymmetricCrypto;
    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.sun.deploy.nativesandbox.comm.Response;
    import com.wf.apicommons.utils.CodeEnum;
    import com.wf.apicommons.utils.CommonResult;
    import com.wf.apicommons.utils.JWTUtils;
    import com.wf.apicommons.utils.MD5Util;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    /**
     * @Description: token鉴权过滤
     */
    @Component
    @Slf4j
    public class AuthJwtFilter implements GlobalFilter, Ordered {
    
        /**
         * 无需鉴权的URL
         */
        private static  final String[] skipAuthUrls={"/user/login","/user/register","/user/getCode/**"};
    
        @Override
        public Mono<Void>
        filter(ServerWebExchange exchange,GatewayFilterChain chain) {
            //获取请求url地址
            String url =exchange.getRequest().getURI().getPath();
            //跳过不需要验证的路径
            if (null != skipAuthUrls && isSkipUrl(url)) {
                return chain.filter(exchange);
            }
    
            //从请求头中取得token
            String token = exchange.getRequest().getHeaders().getFirst("Authorization");
            //标识当前请求来源于网关,屏蔽非法请求
    
            exchange.getRequest().mutate().header("RequestSource", MD5Util.encryption("from_gateway")).build();
    
            CommonResult<String> result=new CommonResult<>();
    
            //判断token是否为null
            if(!StrUtil.hasBlank(token)){
                try{
                    //验证令牌
                    JWTUtils.verify(token);
                    //放行请求
                    return chain.filter(exchange);
                } catch (SignatureVerificationException e) {
                    //无效签名
                    result.setCode(CodeEnum.JWT_INVALID.getCode());
                    result.setData(CodeEnum.JWT_INVALID.getMessage());
                } catch (TokenExpiredException e) {
                    //token过期
                    result.setCode(CodeEnum.JWT_OVERDUE.getCode());
                    result.setData(CodeEnum.JWT_OVERDUE.getMessage());
                } catch (AlgorithmMismatchException e) {
                    //token算法不一致\
                    result.setCode(CodeEnum.JWT_ALGORITHM_INCONSISTENCY.getCode());
                    result.setData(CodeEnum.JWT_ALGORITHM_INCONSISTENCY.getMessage());
                } catch (Exception e) {
                    //token失效
                    result.setCode(CodeEnum.JWT_LOSE_EFFECT.getCode());
                    result.setData(CodeEnum.JWT_LOSE_EFFECT.getMessage());
                }
            }else{
                //无效签名
                result.setCode(CodeEnum.JWT_INVALID.getCode());
                result.setData(CodeEnum.JWT_INVALID.getMessage());
            }
    
    
    
            return getFailResponse(exchange.getResponse(),result);
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    
        /**
         * 判断当前访问的url是否开头URI是在配置的忽略
         * url列表中
         *
         * @param url
         * @return
         */
        public boolean isSkipUrl(String url) {
            for (String skipAuthUrl : skipAuthUrls) {
                if (url.startsWith(skipAuthUrl)) {
                    return true;
                }
            }
            return false;
        }
    
    
        /**
         * 获取失败返回信息
         * @param response
         * @param result
         * @return
         */
        private Mono<Void> getFailResponse(ServerHttpResponse response, CommonResult<String> result) {
            DataBuffer buffer = null;
            try {
            //将map转化成json,response使用的是Jackson
            String resultStr = new ObjectMapper().writeValueAsString(result);
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                response.getHeaders().set("Access-Control-Allow-Origin","*");
                response.setStatusCode(HttpStatus.OK);
                response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                buffer = response.bufferFactory().wrap(resultStr.getBytes("UTF-8"));
            } catch (Exception e) {
                log.info("gateway鉴权错误,{}",e.getMessage());
                e.printStackTrace();
            }
            return response.writeWith(Flux.just(buffer));
        }
    
    }
    
    
    • 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
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    此处的使用到的自定义工具类:
    CodeEnum

    
    import lombok.Data;
    
    /**
     * 状态码枚举
     */
    public enum CodeEnum {
    
        /**操作成功**/
        SUCCESS(200,"操作成功"),
        /**服务调用异常**/
        SERVICE_CALL_EXCEPTION(400,"服务调用异常"),
        /**操作失败**/
        ERROR(500,"操作失败"),
        /**参数不合法**/
        ILLEGAL_PARAMETER(5001,"参数不合法"),
        /**验证码已失效**/
        VERIFICATION_CODE_FAILURE(5002,"验证码已失效"),
        /**用户昵称重复**/
        DUPLICATE_NICKNAME(5003,"用户昵称重复"),
        /**用户名或密码错误**/
        LOGIN_FAILED(5004,"用户名或密码错误"),
        /**文件上传失败**/
        FILE_UPLOAD_FAILED(5005,"文件上传失败"),
        /**资源不存在*/
        RESOURCE_DOES_NOT_EXIST(5006,"资源不存在"),
        /**无效签名**/
        JWT_INVALID(2001,"无效签名"),
        /**token过期**/
        JWT_OVERDUE(2002,"token过期"),
        /**token算法不一致**/
        JWT_ALGORITHM_INCONSISTENCY(2003,"token算法不一致"),
        /**token失效**/
        JWT_LOSE_EFFECT(2004,"token失效"),
        /**非法请求**/
        ILLEGAL_REQUEST(2005,"非法请求,请求来源不合法");
    
        /**
         * 自定义状态码
         **/
        private Integer code;
        /**自定义描述**/
        private String message;
    
        CodeEnum(Integer code, String message){
            this.code = code;
            this.message = message;
        }
    
        public Integer getCode() {
            return code;
        }
        public String getMessage() {
            return message;
        }
    }
    
    • 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

    CommonResult

    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * 请求信息类,用于返回请求是否成功
     * @param 
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T>{
        /**
         * 响应状态码
         */
        private int code;
    
        /**
         * 响应结果描述
         */
        private String message;
    
        /**
         * 返回的数据
         */
        private T data;
    
        /**
         * 成功返回
         * @param data
         * @param 
         * @return
         */
        public static <T> CommonResult<T> success(T data) {
            CommonResult<T> response= new CommonResult<>();
            response.setCode(CodeEnum.SUCCESS.getCode());
            response.setMessage(CodeEnum.SUCCESS.getMessage());
            response.setData(data);
            return response;
        }
    
        /**
         *  失败返回,自定义code
         * @param code
         * @param message
         * @param 
         * @return
         */
        public static <T> CommonResult<T> fail(Integer code, String message) {
            CommonResult<T> response = new CommonResult<>();
            response.setCode(code);
            response.setMessage(message);
            return response;
        }
    
        /**
         *  失败返回
         * @param codeEnum
         * @param 
         * @return
         */
        public static <T> CommonResult<T> fail(CodeEnum codeEnum) {
            CommonResult<T> response = new CommonResult<>();
            response.setCode(codeEnum.getCode());
            response.setMessage(codeEnum.getMessage());
            return response;
        }
        /**
         *  失败返回
         * @param message
         * @param 
         * @return
         */
        public static <T> CommonResult<T> fail(String message) {
            CommonResult<T> response = new CommonResult<>();
            response.setCode(CodeEnum.ERROR.getCode());
            response.setMessage(message);
            return response;
        }
    
    }
    
    
    • 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

    JWTUtils

    
    
    import cn.hutool.jwt.JWTUtil;
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTCreator;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Calendar;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @Description: JWT工具类
     */
    public class JWTUtils {
        //秘钥
        private static final String SIGNATURE = "323123213123213123313";
        //过期时间为1天
       public static  final Integer EXPIRATION_TIME= 1* 24 * 60 * 60;
    
    
    
        /**
         * 生成token
         * @param payload token需要携带的信息
         * @return token字符串
         */
        public static String getToken(Map<String,String> payload){
            // 指定token过期时间为1天
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.SECOND, EXPIRATION_TIME);
            JWTCreator.Builder builder = JWT.create();
            // 构建payload
            payload.forEach((k,v) -> builder.withClaim(k,v));
            // 指定过期时间和签名算法
            return  builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SIGNATURE));
        }
    
    
    
    
        /**
         * 验证token
         * @param token
         */
        public static void verify(String token){
            JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
        }
    
        /**
         * 获取token中payload
         * @param token
         * @return
         */
        public static DecodedJWT getToken(String token){
            return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(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

    MD5Util

    import cn.hutool.core.util.StrUtil;
    import cn.hutool.crypto.SecureUtil;
    
    
    /**
     * @Description: MD5工具类
     */
    public class MD5Util {
    
        /**
         * 加密
         * @param content 需要加密的内容
         * @return
         */
        public static String encryption(String content) {
            if(StrUtil.isNotBlank(content)){
                return SecureUtil.md5(content);
            }
            return null;
        }
    
        /**
         * 验证
         *
         * @param ciphertext 密文
         * @return
         */
        public static boolean verifyingCiphertext(String ciphertext,String content) {
            if(StrUtil.isNotBlank(ciphertext)&&StrUtil.isNotBlank(content)){
                return   ciphertext.equals(encryption(content));
            }
           return false;
        }
    
    
    }
    
    
    • 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

    注:以上代码需要引入胡图工具包

        <!--胡图工具包-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.2</version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 让微服务只允许来自网关的请求

    真实场景中,往往是把微服务放到各个服务器,并不会对外暴露出来的,仅仅会对网关处的服务器进行开放,因此最好的做法就是不会暴露服务,nginx反向代理到网关,网关转发到对应的服务,真正对外提供的仅有nginx

    此处我们在gateway转发请求是在Header中存入一个加密标识(from_gateway),等转发到微服务后再将其获取出来,进行解密,比对,只有正确的才放行

    上述AuthJwtFilter类添加如下代码:

         //标识当前请求来源于网关,屏蔽非法请求
     exchange.getRequest().mutate().header("RequestSource", MD5Util.encryption("from_gateway")).build();
    
    • 1
    • 2

    在9004微服务中新增如下代码:

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.wf.apicommons.utils.CodeEnum;
    import com.wf.apicommons.utils.CommonResult;
    import com.wf.apicommons.utils.MD5Util;
    import org.springframework.web.servlet.HandlerInterceptor;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    
    /**
     * @Description: JWT验证拦截器
     */
    public class JWTInterceptor implements HandlerInterceptor {
    
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            CommonResult<String> result=new CommonResult<>();
            
            //获取请求来源
            String requestSource = request.getHeader("RequestSource");
            boolean decrypt = MD5Util.verifyingCiphertext(requestSource,"request_from_gateway");
            //验证请求来源是否为网关转发
            if(decrypt){
                //放行请求
                return true;
            }else{
                result.setCode(CodeEnum.ILLEGAL_REQUEST.getCode());
                result.setData(CodeEnum.ILLEGAL_REQUEST.getMessage());
            }
            
            //将map转化成json,response使用的是Jackson
            String json = new ObjectMapper().writeValueAsString(result);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            response.setHeader("Access-Control-Allow-Origin","*");
            response.getWriter().print(json);
            return false;
        }
    }
    
    
    • 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

    测试:
    请求:http://localhost:9004/test
    在这里插入图片描述
    返回结果:
    在这里插入图片描述
    通过网关测试:
    请求:http://localhost:9005/message/board/test

    在这里插入图片描述
    返回结果:
    在这里插入图片描述

  • 相关阅读:
    GCC特性——内建函数
    springboot项目生成war包并部署到Tomcat服务器
    RPC 框架设计 四、Netty高级应用
    Nucleic Acids Research | AlphaFold 蛋白质结构数据库
    Docker 学习笔记一
    测试环境搭建整套大数据系统(十四:搭建mysql8)
    echarts使用custom类型绘制矩形
    【算法】记忆化搜索
    Python数学计算工具3、Python 斐波那契数列-前500项列表
    为什么要引入线程?
  • 原文地址:https://blog.csdn.net/qq_46122292/article/details/126664560