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

  • 搭建前提

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

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

1.导入依赖

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

  <dependencies>
        <!--网关gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- openfeign服务调用 此处是避免依赖冲突才引入的-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 服务注册/发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 引入自己定义的api-commons,这样就可以使用共有类 ,注意将自己的依赖包中排除mvc相关依赖,避免依赖传递-->
        <dependency>
            <groupId>com.wf</groupId>
            <artifactId>api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

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("网关微服务启动成功");
    }
}

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

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

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));
    }

}

此处的使用到的自定义工具类:
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;
    }
}

CommonResult


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 请求信息类,用于返回请求是否成功
 * @param <T>
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
    /**
     * 响应状态码
     */
    private int code;

    /**
     * 响应结果描述
     */
    private String message;

    /**
     * 返回的数据
     */
    private T data;

    /**
     * 成功返回
     * @param data
     * @param <T>
     * @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 <T>
     * @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 <T>
     * @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 <T>
     * @return
     */
    public static <T> CommonResult<T> fail(String message) {
        CommonResult<T> response = new CommonResult<>();
        response.setCode(CodeEnum.ERROR.getCode());
        response.setMessage(message);
        return response;
    }

}

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);
    }

}

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;
    }


}

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

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

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

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

上述AuthJwtFilter类添加如下代码:

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

在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;
    }
}

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

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

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亿只王菜菜

各位爷,赏口饭吃吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值