Gateway实现服务路由、服务限流、使用feign进行服务调用、用户登录token鉴权及跨域问题修复(基于JWT下发)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本文主要是基于SpringCloud Gateway实现服务路由、服务限流、使用feign进行服务调用、用户登录token鉴权及跨域问题修复。测试使用JWT进行token下发。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Gateway和Nginx区别是什么?

SpringCloudGateway : 微服务网关,事项微服务的统一路由,统一鉴权,跨域,限流等功能

Nginx :高性能HTTP和反向代理的web服务器,处理高并发能力是十分强大,最高能支持5w个并发连接数。

二、使用步骤

1.引入库

代码如下(示例):如无限流和使用feign需求,可以将相关依赖进行剔除

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.neq</groupId>
    <artifactId>gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
        <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.neq</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--基于Redis实现限流-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>
        
		<!--feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.yaml配置

代码如下(示例):没有跨域、限流需求可以将相关配置去除

server:
  port: 8989
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
       #跨域问题通过配置解决
      globalcors:
        cors-configurations:
          '[/**]':
            allowCredentials: true
            allowedHeaders: '*'
            allowedMethods: '*'
            allowedOrigins: '*'
       #服务路由
      routes:
        - id: demoService
          uri: lb://demo-service
          predicates: Path=/demo/**
          filters:
            - StripPrefix=1
            #根据ip进行限流
            - name: RequestRateLimiter
              args:
                key-resolver: '#{@ipKeyResolver}'
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 1
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8765/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}


3.启动类增加

package com.neq.gateway;

import com.neq.gateway.config.IpKeyResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

@SpringBootApplication(scanBasePackages = {"com.neq"})
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.neq.gateway.feign"})
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean(name = "ipKeyResolver")
    public KeyResolver userIpKeyResolver() {
        return new IpKeyResolver();
    }
}

4.根据ip进行服务限流

package com.neq.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class IpKeyResolver implements KeyResolver {

    /***
     * 根据IP限流
     * @param exchange
     * @return
     */
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

5.跨域问题解决

跨域有两种解决方式,一种是上文中通过在yaml配置文件增加相关配置,还有一种是通过增加配置类,其实本质一样

package com.neq.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * 跨域配置,目前两种解放方式。1、yml配置文件增加跨域配置 2、使用当前类
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*"); // 允许任何方法(post、get等)
        config.addAllowedOrigin("*"); // 允许任何域名使用
        config.addAllowedHeader("*"); // 允许任何头
        config.setAllowCredentials(true); //允许接受cookie

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

}

6.gateway使用feign失败问题修复

堆栈信息

feign.codec.DecodeException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at feign.AsyncResponseHandler.decode(AsyncResponseHandler.java:119) ~[feign-core-10.10.1.jar:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/java/sayHi" [ExceptionHandlingWebHandler]

解决方式

package com.neq.gateway.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

import java.util.stream.Collectors;
@Configuration
public class BeanConfig {

    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

然后就可以直接编写feign接口了

package com.neq.gateway.feign;

import com.alibaba.fastjson.JSONObject;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("demo-service")
public interface DemoFeign {

    @GetMapping(value = "/user/testFeign")
    JSONObject testFeign(@RequestParam("username") String username);
}

7.token解析及鉴权

这里有用到JWTUtil,我这边直接给出

package com.neq.common.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.neq.common.entity.UserInfo;
import com.neq.common.enums.ResultCode;
import com.neq.common.exception.CustomException;

import java.util.Date;

public class JWTUtil {

    private static final String USERNAME = "username";
    private static final String COMPNAY_ID = "companyId";

    private static final String issuser = "user-service";
    private static final String secretKey = "!@#$%^&*";
    private static final String BEARER = "Bearer ";

    /**
     * 生成Token
     * @param issuser    签发者
     * @param username   用户标识(唯一)
     * @param secretKey  签名算法以及密匙
     * @param tokenExpireTime 过期时间
     * @return
     */
    public static String generateToken(String username, Long companyId, long tokenExpireTime) {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);

        Date now = new Date();

        Date expireTime = new Date(now.getTime() + tokenExpireTime);

        String token = JWT.create()
                .withIssuer(issuser)
                .withIssuedAt(now)
                .withExpiresAt(expireTime)
                .withClaim(USERNAME, username)
                .withClaim(COMPNAY_ID, companyId)
                .sign(algorithm);

        return token;
    }

    /**
     * 校验Token
     * @param issuser   签发者
     * @param token     访问秘钥
     * @param secretKey 签名算法以及密匙
     * @return
     */
    public static void verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secretKey);
            JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(issuser).build();
            jwtVerifier.verify(token);
        } catch (JWTDecodeException jwtDecodeException) {
            throw new CustomException(ResultCode.TOKEN_INVALID.getCode(), ResultCode.TOKEN_INVALID.getMessage());
        } catch (SignatureVerificationException signatureVerificationException) {
            throw new CustomException(ResultCode.TOKEN_SIGNATURE_INVALID.getCode(), ResultCode.TOKEN_SIGNATURE_INVALID.getMessage());
        } catch (TokenExpiredException tokenExpiredException) {
            throw new CustomException(ResultCode.TOKEN_EXPIRED.getCode(), ResultCode.TOKEN_INVALID.getMessage());
        } catch (Exception ex) {
            throw new CustomException(ResultCode.UNKNOWN_ERROR.getCode(), ResultCode.UNKNOWN_ERROR.getMessage());
        }
    }

    /**
     * 从Token中提取用户信息
     * @param token
     * @return
     */
    public static UserInfo getUserInfo(String token) {
        DecodedJWT decodedJWT = JWT.decode(token);
        String username = decodedJWT.getClaim(USERNAME).asString();
        Long companyId = decodedJWT.getClaim(COMPNAY_ID).asLong();
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername(username);
        userInfo.setCompanyId(companyId);
        return userInfo;
    }

}

拦截器内进行token解析和鉴权

package com.neq.gateway.filter;

import com.neq.common.entity.UserInfo;
import com.neq.common.utils.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.Mono;

import java.util.ArrayList;
import java.util.List;

@Component
@Slf4j
public class LoginTokenFilter implements GlobalFilter, Ordered {

    private static final String AUTHORIZE_TOKEN = "Authorization";
    private static final String BEARER = "Bearer ";

    private static List<String> whiteList = new ArrayList<>();
    static {
        whiteList.add("/demo/user/loginNew");
        whiteList.add("/demo/user/register");
    }


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        log.info("当前环境已开启token校验");
        ServerHttpRequest request = exchange.getRequest();

        String path = request.getURI().getPath();
        if(whiteList.contains(path)) {
            return chain.filter(exchange);
        }

        HttpHeaders headers = request.getHeaders();
        ServerHttpResponse response = exchange.getResponse();
        // 取Authorization
        String tokenHeader = headers.getFirst(AUTHORIZE_TOKEN);
        // token不存在
        if (StringUtils.isEmpty(tokenHeader)) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 取token
        String token = this.getToken(tokenHeader);
        log.info("token=" + token);

        // token不存在
        if (StringUtils.isEmpty(token)) {
            log.info("token不存在");
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        JWTUtil.verifyToken(token);
        UserInfo userInfo = JWTUtil.getUserInfo(token);
        //这里请求user服务获取用户,暂时不需要
        //将解析的用户信息放置到request中
        request.mutate().headers(httpHeaders -> httpHeaders.add("nickName", userInfo.getUsername())).build();
        request.mutate().headers(httpHeaders -> httpHeaders.add("companyId", userInfo.getCompanyId().toString())).build();
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -10;
    }


    /**
     * 解析Token
     */
    public String getToken(String requestHeader) {
        //2.Cookie中没有从header中获取
        if (requestHeader != null && requestHeader.startsWith(BEARER)) {
            return requestHeader.substring(7);
        }
        return "";
    }
}

放到request的用户信息可以编写util类,在后续服务中直接获取

package com.neq.common.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class RequestUtil {

    public static Long getUserId() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Long userId = (Long) request.getAttribute("accountId");
        return userId;
    }

    public static String getNickName() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String nickName = request.getHeader("nickName");
        return nickName;
    }

    public static Long getCompanyId() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Long companyId = Long.valueOf(request.getHeader("companyId"));
        return companyId;
    }

    public static String getToken() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = (String) request.getAttribute("token");
        return token;
    }
}

    @Override
    public TestUser userInfo(TestUser user) {
        Long companyId = RequestUtil.getCompanyId();
        QueryWrapper qw = new QueryWrapper();
        qw.eq("nick_name", user.getNickName());
        qw.eq("company_id", companyId);
        TestUser testUser = this.baseMapper.selectOne(qw);
        if(Objects.isNull(testUser)) {
            throw new CustomException("用户不存在");
        }
        testUser.setPassword("");
        return testUser;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值