提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
本文主要是基于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;
}