This is the fourth day of my participation in the August More text Challenge. For details, see:August is more challenging
Reading reminder:
- This article is intended for those with some springBoot background
- The Hoxton RELEASE of Spring Cloud is used in this tutorial
- Since Knife4j is more friendly than Swagger, this article integrates Knife4j
- This article relies on the project from the previous article, so check out the previous article for a seamless connection, or download the source directly: github.com/WinterChenS…
Before a summary
- The beginning of the SpringCloud series (part 1)
- Nacos of SpringCloud series (2) | August more challenges
- # SpringCloud series (3) of the Open Feign | August more challenges
- SpringCloud series (4) the SpringCloud Gateway | August more challenges
This paper gives an overview of
- Spring Cloud Gateway integrates with Knife4j
- Spring Cloud Gateway integrated login permission verification
start
The previous article introduced the use of Spring Cloud Gateway. This article will show you how to aggregate Swagger documents at the Gateway layer, which is a very convenient way to manage development documents and is a common way in the industry.
Add knife4j dependencies to the parent POM file dependencyManagement node:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
Copy the code
Configure spring-cloud-nacos-Provider and Spring-cloud-nacos-Consumer
Note that the two projects in the title were built in previous articles. If you don’t want to see the previous articles, create two modules and integrate them with NACOS and Knife4j respectively.
Add maven dependencies to each module:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
Copy the code
Add configuration class Knife4jConfiguration in each module
@Configuration
public class Knife4jConfiguration {
@Value("${swagger.enable:true}")
private boolean enableSwagger;
@Bean(value = "defaultApi2")
public Docket createRestApi(a) {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("Service provider")
.version("1.0")
.build())
.enable(enableSwagger)
.select()
.apis(RequestHandlerSelectors.basePackage("com.winterchen.nacos.rest")) .paths(PathSelectors.any()) .build(); }}Copy the code
Note that some relevant information needs to be modified.
New configuration:
swagger:
enable: true
Copy the code
Configure the spring – cloud – gateway
Next is the highlight, how to aggregate Swagger documents in Spring Cloud Gateway.
Add maven dependencies to the POM first:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
Copy the code
Added Swagger configuration class: SwaggerResourceConfig
@Component
@Primary
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
public SwaggerResourceConfig(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
this.routeLocator = routeLocator;
this.gatewayProperties = gatewayProperties;
}
@Override
public List<SwaggerResource> get(a) {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
// Obtain the ids of all routes
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
// Filter out the routes defined in the configuration file -> filter out Path Route Predicate-> concatenate the api-docs Path -> Generate SwaggerResourcegatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> { route.getPredicates().stream() .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("* *"."v2/api-docs"))));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
returnswaggerResource; }}Copy the code
The main configuration functions are already commented in the code.
Add a new controller: SwaggerHandler
@RestController
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
/** * Swagger Security configuration, support oAuth and apiKey Settings */
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/** * Swagger UI configuration */
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/** * Swagger Resource configuration, microservices in each service API-docs information */
@GetMapping("/swagger-resources")
public Mono<ResponseEntity> swaggerResources(a) {
return Mono.just((newResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); }} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --Copy the code
test
Spring-cloud-nacos-provider, Spring-cloud-nacos-Consumer and Spring-cloud-Gateway are three services.
Open the swagger of address: http://127.0.0.1:15010/doc.html
Verification results:
The result in the figure above indicates that the aggregation succeeded.
Integrated verification of login rights
In order to direct the theme, this integration login permission will be as simple as possible, which ignores some details, such as login service module, you can view the source code of demo: github.com/WinterChenS…
And I’ll be clear on the key points.
Before you start, you need to understand one principle: to implement unified authentication, all requests need to be intercepted in a uniform manner. To implement unified interception, the Spring Cloud Gateway has a filter interface: GlobalFilter.
So we can intercept requests by implementing the GlobalFilter interface.
implementation
Create a new class to implement the GlobalFilter interface and implement the filter method. On this basis, we also need to implement the Ordered interface, control the priority of interception, authentication interception priority is the highest.
@Slf4j
@Component
public class AuthorizeFilter implements GlobalFilter.Ordered {
@Autowired
UserRedisCollection userRedisCollection;
/ / (1)
private boolean checkWhiteList(String uri) {
boolean access = false;
if (uri.contains("/login") || uri.contains("/v2/api-docs")) {
access = true;
if (uri.contains("logout")) {
access = false; }}if (uri.contains("/open-api")) {
access = true;
}
return access;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/ / (2)
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String uri = request.getURI().getPath();
/ / (3)
// The front end cannot access the header
response.getHeaders().add("Access-Control-Allow-Headers"."X-PINGOTHER, Origin, X-Requested-With, Content-Type, Accept, token");
response.getHeaders().add("Access-Control-Expose-Headers"."token");
ServerHttpRequest mutableReq = request.mutate()
.header(DefaultConstants.IP_ADDRESS, getIpAddress(request))
.build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
/ / (4)
// Check the whitelist
if (checkWhiteList(uri)) {
return chain.filter(mutableExchange);
}
/ / (5)
// Obtain the token from the request
String accessToken = request.getHeaders().getFirst(DefaultConstants.TOKEN);
log.info("AccessToken: [{}]", accessToken);
/ / (6)
if (StringUtils.isBlank(accessToken)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "Not logged in");
}
Claims claims = null;
try {
/ / (7)
claims = JwtUtil.parseJWT(accessToken, DefaultConstants.SECRET_KEY);
if (claims == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "Not logged in");
}
log.info("claims is:{}", claims);
if (claims.getSubject().equals(DefaultConstants.USER)){
if(claims.get(DefaultConstants.USERID)! =null) {
Long userId = Long.parseLong(claims.get(DefaultConstants.USERID).toString());
log.info("userId:{}", userId);
Map<String, Object> map = Maps.newHashMapWithExpectedSize(1);
map.put(DefaultConstants.USERID,userId.toString());
/ / (8)
UserInfoEntity userInfo = userRedisCollection.getAuthUserInfoAndCache(userId);
// Determine whether
if (userInfo == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "Not logged in");
}
/ / (9)
String token = JwtUtil.createToken(DefaultConstants.USER, map, DefaultConstants.SECRET_KEY);
response.getHeaders().add(DefaultConstants.TOKEN, token);
mutableReq = request.mutate().header(DefaultConstants.USER_ID, String.valueOf(userId))
.header(DefaultConstants.IP_ADDRESS, getIpAddress(request))
.build();
mutableExchange = exchange.mutate().request(mutableReq).build();
returnchain.filter(mutableExchange); }}}catch (Exception e) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "Not logged in");
}
return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "Not logged in");
}
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse, ResultCodeEnum resultCode, String responseText) {
serverHttpResponse.getHeaders().add("Content-Type"."application/json; charset=UTF-8"); CommonResult<? > result = CommonResult.failed(resultCode.getCode(), responseText); DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(result).getBytes());return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
public String getIpAddress(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeaders().getFirst("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddress().getHostString();
}
return ip;
}
@Override
public int getOrder(a) {
return -100; }}Copy the code
- (1) This method adds whitelists to the list, and does not verify permissions when requests are made to these urls
- (2) When we use Spring Cloud Gateway, we notice that filters (including GatewayFilter, GlobalFilter and GatewayFilterChain) all rely on ServerWebExchange. The design of the filter is similar to the filter in Servlet, that is, the logic that the current filter determines whether to execute the next filter. ServerWebExchange is the context of the current request and response, including not only request and response, but also some extension methods. For example, the request and response can be obtained in the code.
- (3) This step is mainly to solve the problem that the front-end fails to obtain the required parameters in the header. Such as
token
。 - (4) Here corresponds to the whitelist judgment in the first step. If the current request is in the whitelist, the following judgments are skipped and the next filter is directly executed.
- (5) It is very simple to obtain the token from the request, which is convenient for JWT to convert into the corresponding user information.
- (6) If the request does not carry a token, it will not be passed.
- (7) JWT converts the token into user information for subsequent judgment and basic information of the user.
- (8) After the user information is obtained above, the user data is queried in Redis according to the userId of the user. If the user information does not exist, it means that the user information has expired, and the timeout period will be reset when the user information is queried from Redis (which ensures that the user does not need to log in again as long as the user is online frequently. If you are not online for a specified period of time, you need to log in again. Note: This is to linkage with the login service, that is, after successful login, the user’s information will be stored in Redis, and then the gateway can get the user’s information. So you need to make sure of that.
- (9) The token will be reissued to prevent the token from expiring. The expiration time of the token can be set in jwtUtils. Therefore, the front-end needs to use the new token as the token for the next request after each request.
- Note: The token in this article is placed in the header, and the front partner needs to fetch the token from the header.
In the code methods: UserRedisCollection. GetAuthUserInfoAndCache (Long userId)
@Autowired
private RedisTemplate redisTemplate;
public UserInfoEntity getAuthUserInfoAndCache(Long userId) {
CommonAssert.meetCondition(userId == null."UserId not obtained");
String key = DefaultConstants.USER_INFO_REDIS + userId;
UserInfoEntity entity = (UserInfoEntity) redisTemplate.opsForValue().get(key);
if (null! = entity) { redisTemplate.opsForValue().set(key, entity,60 * 24 * 60 * 60 * 1000, TimeUnit.MILLISECONDS);
return entity;
}
CommonAssert.meetCondition(true."The current user is not logged in. No login information is obtained.");
return null;
}
Copy the code
This method is simple. The user information is obtained based on the userId. After the user information is obtained successfully, the timeout period is reset.
The login service for this article can be obtained from the Demo source:
Spring-cloud-hoxton-study/Spring-cloud-auth at main · WinterChenS/ Spring-cloud-Hoxton-study
test
Start the Gateway, Provider, and Auth services respectively.
First, test if you are not logged in:
Then go to the Auth service and open the login interface:
Open Headers and copy the token
Switch to the Provider service again and set the global parameters for the document:
Then refresh the page and request again, you can see that the request succeeded:
At this point, gateway authentication is successfully integrated.
expand
- As for the logout logic, the idea is that you only need to delete the user information from redis in auth service. Then you can query redis in gateway and find that the user login information is invalid.
- How do I get the currently logged in user information from the service? If the userId is set to the token, then the token is converted to user information. Then the user information is obtained from redis based on the userId. If the userId is set to the token, then the user information is obtained from redis. UserId = userId = userId = userId = userId
public static String getUserIdByCurrent(a) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if(attributes ! =null) {
HttpServletRequest request = attributes.getRequest();
return request.getHeader(ConstantsUtil.USER_ID);
}else{
return ""; }}Copy the code
conclusion
This chapter describes how to aggregate Swagger documentation and unified authentication integration in gateway. This chapter is not much. Originally intended to be separated into two parts, the swagger section has no principles to pay attention to, so just put it together. The really important point is the application of filter in Spring Cloud Gateway, which can be understood in detail by searching for information. The next article will introduce how to use the flow limiting component Sentinel, which is provided by Alibaba.
The source address
Github.com/WinterChenS…
reference
Spring Cloud Gateway-ServerWebexChange Changes to core methods and request or response content