Spring Cloud -Hoxton.RELEASE(十):授权中心-Spring Cloud Oauth2+JWT

Spring Security

Spring Security的官网介绍对Spring Security描述如下:

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求。

简单来讲官方对Spring Securuty定位就是身份验证和授权框架。

Oauth2

Oauth2的官网介绍对Oauth2描述如下:

OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 supersedes the work done on the original OAuth protocol created in 2006. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0取代了在2006年创建的原始OAuth协议上所做的工作。OAuth2.0专注于简化客户端开发人员,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的授权流。

简单来讲Oauth2就是授权协议
我们这里用Spring Security和Oauth2协议完成权限框架的设计。

用户和客户端信息存在内存,token存在Redis

新建父项目spring-cloud-oauth2-demo

pom文件如下:

<?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 http://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.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>pom</packaging>

<groupId>xyz.liuzhuoming</groupId>
<artifactId>spring-cloud-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
</properties>

<modules>
<module>eureka</module>
<module>auth</module>
<module>client</module>
<module>gateway</module>
</modules>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

其中eureka项目为注册中心,搭建方式不再赘述,自行创建,定义端口号为8765。

创建网关Gateway并注册到eureka

新建项目gateway,pom如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>xyz.liuzhuoming</groupId>
<artifactId>spring-cloud-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

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

</project>

application.yml如下(配置参数前文以作讲解,不再赘述):

server:
port: 5432

spring:
application:
name: @pom.artifactId@
redis:
host: localhost
port: 6379
password:
timeout: 500
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
routes:
- id: auth
uri: lb://AUTH
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
key-resolver: "#{@hostnameKeyResolver}"
- id: client
uri: lb://CLIENT
predicates:
- Path=/client/**
filters:
- StripPrefix=1
- Authorization=true

eureka:
client:
service-url:
defaultZone: http://localhost:8765/eureka/

management:
endpoints:
web:
exposure:
include: httptrace,info,health

新建权限校验过滤器和过滤器工厂类:

package xyz.liuzhuoming.gateway.filter;

import java.util.Objects;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* 请求权限局部过滤器
*
* @author liuzhuoming
*/
public class AuthorizationGatewayFilter implements GatewayFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
String token = Objects
.requireNonNull(exchange.getRequest().getHeaders().get("Authorization")).get(0);
if (token == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
package xyz.liuzhuoming.gateway.filter.factory;

import xyz.liuzhuoming.gateway.filter.AuthorizationGatewayFilter;
import java.util.Collections;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;

/**
* 请求权限局部过滤器工厂
*
* @author liuzhuoming
*/
public class AuthorizationGatewayFilterFactory extends
AbstractGatewayFilterFactory<AuthorizationGatewayFilterFactory.Config> {

public AuthorizationGatewayFilterFactory() {
super(Config.class);
}

@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("enabled");
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (!config.isEnabled()) {
return chain.filter(exchange);
} else {
return new AuthorizationGatewayFilter().filter(exchange, chain);
}
};
}

public static class Config {

/**
* 是否开启鉴权header验证
*/
private boolean enabled;

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}

网关配置类:

package xyz.liuzhuoming.gateway.config;

import xyz.liuzhuoming.gateway.filter.factory.AuthorizationGatewayFilterFactory;
import java.util.Objects;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
* spring cloud config
*
* @author liuzhuoming
*/
@Configuration
public class SpringCloudConfig {

@Bean
KeyResolver hostnameKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName());
}

@Bean
AuthorizationGatewayFilterFactory authorizationGlobalFilterFactory() {
return new AuthorizationGatewayFilterFactory();
}
}

创建授权服务并注册到eureka

新建项目auth,pom文件如下(其中redis为存储token):

<?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>xyz.liuzhuoming</groupId>
<artifactId>spring-cloud-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

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

application.yml为:

server:
port: 6543

spring:
application:
name: @pom.artifactId@
redis:
host: localhost
port: 6379
database: 0

eureka:
client:
service-url:
defaultZone: http://localhost:8765/eureka/

启动类添加@EnableResourceServer和@EnableDiscoveryClient注解,其中第一个注解是标明服务提供了资源服务,第二个注解就不解释了:

package xyz.liuzhuoming.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@SpringBootApplication
@EnableResourceServer
@EnableDiscoveryClient
public class AuthApplication {

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

}

然后新建Bean注册类SpringCloudConfig:

package xyz.liuzhuoming.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* spring cloud config
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
public class SpringCloudConfig {

@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}

新建授权服务配置类AuthorizationServerConfigurer:

package xyz.liuzhuoming.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
* 授权服务配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
PasswordEncoder passwordEncoder;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String secret = passwordEncoder.encode("123456");

//在内存模拟生成两个Oauth2客户端
clients.inMemory()
//客户端id和密钥
.withClient("client_1").secret(secret)
//客户端授权类型
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
//授权范围
.scopes("all")
.and()
.withClient("client_2").secret(secret)
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
.scopes("all");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
//token存储方式
.tokenStore(tokenStore())
//密码授权模式的授权管理器
.authenticationManager(authenticationManager)
//允许的token请求类型
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
//允许客户端表单授权
.allowFormAuthenticationForClients();
}

@Bean
public TokenStore tokenStore() {
//定义token存储方式为redis存储
return new RedisTokenStore(redisConnectionFactory);
}
}

新建用来添加模拟用户的类WebSecurityConfigurer:

package xyz.liuzhuoming.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Autowired
PasswordEncoder passwordEncoder;

@Bean
@Override
protected UserDetailsService userDetailsService() {
String secret = passwordEncoder.encode("123456");

//在内存模拟生成两个用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User
//用户名
.withUsername("user_1")
//密码
.password(secret)
//权限
.authorities("a:get")
.build());
manager.createUser(User
.withUsername("user_2")
.password(secret)
.authorities("b:get")
.build());
return manager;
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

创建Oauth2客户端服务并注册到eureka

新建项目client,pom如下:

<?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>xyz.liuzhuoming</groupId>
<artifactId>spring-cloud-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>client</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

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

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

application.yml配置如下:

server:
port: 7654

spring:
application:
name: client

eureka:
client:
service-url:
defaultZone: http://localhost:8765/eureka/

security:
oauth2:
resource:
#从oauth2服务获取user信息接口
user-info-uri: http://localhost:5432/auth/user/current
client:
#oauth2客户端id
id: client_2
#oauth2客户端密钥
client-secret: 123456
#oauth2服务端获取token接口
access-token-uri: http://localhost:5432/auth/oauth/token
#oauth2客户端请求授权类型
grant-type: client_credentials,password
#oauth2客户端授权范围
scope: all

在启动类添加注册服务注解:

package xyz.liuzhuoming.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {

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

}

oauth2客户端配置类:

package xyz.liuzhuoming.client.config;

import feign.RequestInterceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;

/**
* Oauth2客户端配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfigurer {

@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}

@Bean
public RequestInterceptor oauth2FeignRequestInterceptor() {
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
clientCredentialsResourceDetails());
}

@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}

资源服务配置类:

package xyz.liuzhuoming.client.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
* 资源服务配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {


@Override
public void configure(HttpSecurity http) throws Exception {
http
//配置访问控制
.authorizeRequests()
//不需要认证
.antMatchers("/actuator/**").permitAll()
//必须认证后才可以访问
.antMatchers("/a/**", "/b/**").authenticated();
}
}

测试资源接口:

package xyz.liuzhuoming.client.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 测试
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@RestController
@Slf4j
public class TestController {

@GetMapping("/a")
@PreAuthorize("hasAuthority('a:get')")
public String a() {
return "a";
}

@GetMapping("/b")
@PreAuthorize("hasAuthority('b:get')")
public String b() {
return "b ";
}
}

然后按照顺序启动eureka,gateway,auth,client,并用Postman请求http://localhost:5432/auth/oauth/token?username=user_2&password=123456&grant_type=password&scope=all,并在Postman的Authorization参数选择Basic Auth参数,并填写oauth2客户端id和密钥:
01.jpg
请求之后返回结果:

{
"access_token": "ae9baceb-c27a-4f9a-bf02-f13aa7c18f5f",
"token_type": "bearer",
"refresh_token": "994c5f1b-2732-452a-8f15-b92b62783deb",
"expires_in": 41220,
"scope": "all"
}

获取到access_token之后用Postman请求http://localhost:5432/client/b,并在Postman的Authorization参数选择Bearer Token参数,并填写刚才获得的access_token:
02.jpg
获得结果:

b

之后用Postman请求http://localhost:5432/client/a,并在Postman的Authorization参数选择Bearer Token参数,并填写刚才获得的access_token,得到结果:

{
"error": "access_denied",
"error_description": "不允许访问"
}

可以看到权限配置已经生效了,只有有对应authorities的用户才可以访问对应authorities的资源。

用户和客户端信息和token存在数据库

前面是把用户和客户端信息放在内存的,实际项目当然不会这样做,token存在redis是可以的就不做修改了,把用户和客户端信息存在数据库。
eureka和gateway项目不需要修改。
首先在mysql数据库建表名为oauth2,下载sql文件并导入(已经插入了部分用户信息和客户端信息)(点击下载)。
其中user,user_role,role,role_authority,authority五张表可以自行定制,其他表是Spring Security官方推荐的表结构,没必要就尽量不做修改。
然后修改auth项目加入mysql驱动和mybatis-plus依赖,pom文件修改如下“

<?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>xyz.liuzhuoming</groupId>
<artifactId>spring-cloud-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>

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

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

application.yml修改如下:

server:
port: 6543

spring:
application:
name: @pom.artifactId@
redis:
host: localhost
port: 6379
database: 0
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf8&useSSL=false&noAccessToProcedureBodies=true&serverTimezone=Asia/Shanghai
username: root
password: root
main:
#允许覆盖已定义的bean
allow-bean-definition-overriding: true

eureka:
client:
service-url:
defaultZone: http://localhost:8765/eureka/

再修改授权配置类,主要是修改tokenStore:

package xyz.liuzhuoming.auth.configurer;

import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

/**
* 授权服务配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableAuthorizationServer
@Slf4j
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private ClientDetailsService clientDetailsService;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
//token存储方式
tokenServices.setTokenStore(tokenStore);
//是否允许token刷新
tokenServices.setSupportRefreshToken(true);
//客户端Service
tokenServices.setClientDetailsService(clientDetailsService);
//token有效时间(秒)
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(30));

endpoints
//token存储方式
.tokenStore(tokenStore())
//密码授权模式的授权管理器
.authenticationManager(authenticationManager)
//用户Service
.userDetailsService(userDetailsService)
//tokenService
.tokenServices(tokenServices);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients();
;
}

@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
//修改token存储方式为jdbc
// return new JdbcTokenStore(dataSource);
}

@Bean
public ClientDetailsService clientDetailsService() {
//设置客户端信息存储方式为jdbc
return new JdbcClientDetailsService(dataSource);
}
}

WebSecurityConfigurer类删除模拟用户方法:

package xyz.liuzhuoming.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
```
添加用户实体类:
```java
package xyz.liuzhuoming.auth.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Collection;
import java.util.List;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
* 用户
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Data
@TableName("user")
public class User implements UserDetails {

private static final long serialVersionUID = -8630757683959992293L;
@TableId(type = IdType.AUTO)
private Integer id;
@TableField
private String username;
@TableField
private String password;
@TableField(exist = false)
private List<Authority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

添加权限实体类:

package xyz.liuzhuoming.auth.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;

/**
* 权限
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Data
@EqualsAndHashCode
public class Authority implements GrantedAuthority {

private static final long serialVersionUID = 1179377491735761716L;
private Long id;
private String name;
private String authority;

@Override
public String getAuthority() {
return authority;
}
}

新建用户Mapper:

package xyz.liuzhuoming.auth.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import xyz.liuzhuoming.auth.entity.Authority;
import xyz.liuzhuoming.auth.entity.User;

/**
* 用户
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {

/**
* 根据用户名获取权限集合
*
* @param username 用户名
* @return 权限集合
*/
List<Authority> selectAuthoritiesByUsername(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.liuzhuoming.auth.mapper.UserMapper">

<select id="selectAuthoritiesByUsername" parameterType="string"
resultType="xyz.liuzhuoming.auth.entity.Authority">
SELECT a.id,a.`name`,a.authority
FROM authority a
LEFT JOIN role_authority ra ON ra.authority_id=a.id
LEFT JOIN user_role ur ON ra.role_id=ur.role_id
LEFT JOIN `user` u ON ur.user_id=u.id
WHERE u.username=#{username}
</select>
</mapper>

修改用户service为:

package xyz.liuzhuoming.auth.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import xyz.liuzhuoming.auth.entity.Authority;
import xyz.liuzhuoming.auth.entity.User;
import xyz.liuzhuoming.auth.mapper.UserMapper;

/**
* 用户
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUsername, username);
//根据用户名查询用户查询
User user = userMapper.selectOne(userLambdaQueryWrapper);
//根据用户名查询权限集合
List<Authority> authorityList = userMapper.selectAuthoritiesByUsername(username);
user.setAuthorities(new ArrayList<>(new HashSet<>(authorityList)));
return user;
}
}

重启auth项目,进行测试,测试方法同上,请自行尝试,不再赘述。

修改token类型为JWT,并在gateway进行简单的权限校验

JWT官网对JWT描述如下:

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。

简单来讲就是个令牌标准

使用keytool进行oauth2授权服务和客户端的非对称加密

openSsl可能windows会提示没有这个命令,所以可以在linux环境下生成jks密钥库文件和公钥。
jks即Java KeyStore(Java密钥库)。
首先运行keytool -genkeypair -alias jwt -keyalg RSA -dname "CN=jwt,OU=j,O=wt,L=hz,S=zj,C=CH" -keypass 654321 -keystore jwt.jks -storepass 654321生成jks文件,其中alias参数意思是密钥库别名,keyalg加密方式,dname发行者信息(按顺序分别为姓名、组织机构名、组织名、城市名、省份名、国家名,可以删除cname参数及值来按顺序填入),keypass密钥密码,keystore密钥库文件名,storepass密钥库密码。会在当前文件夹生成jwt.jks文件(可能会提示转换行业标准之类的,可以转换也可以不转换,不影响接下来的操作),再然后运行keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey从jwt.jks提取出密钥信息,类似:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtNIqpyseTONuqBVJfGyN
guo74rDyxMHV8TAsLwrQpr/3YrW5ilETc9UjtA5L6ls9UctlgYf0dFwtLfiCdCei
MP5GsBZYzKpUTAxcly7NkR96eDUUBRivh0+Qo3MRwTfCucCoxCkUJ67SvYkcwDQC
H5d0CdfE12aFVNrdC/oG7J0S9N4YQwPVyEzNQdlN3SNfLKqQ6MmmrCHdxZ+IlN6f
68zMMFCfcXIRC90UNpqYRS6lhrZhBc1apqtJiMmiQBdF0n1+nTrugUjA1+W0Sx9e
hPwWSYY0GzU+e+yItGkUjRPMkB68/sX0tVgU2Z+rCWQm6Xcdy2Mq/ZWDiHrwAyuT
5QIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDOzCCAiOgAwIBAgIEXWTtbTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJD
SDELMAkGA1UECBMCemoxCzAJBgNVBAcTAmh6MQswCQYDVQQKEwJ3dDEKMAgGA1UE
CxMBajEMMAoGA1UEAxMDand0MB4XDTE5MTAyMjA1NDY0NloXDTIwMDEyMDA1NDY0
NlowTjELMAkGA1UEBhMCQ0gxCzAJBgNVBAgTAnpqMQswCQYDVQQHEwJoejELMAkG
A1UEChMCd3QxCjAIBgNVB0sTAWoxDDAKBgNVBAMTA2p3dDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBALTSKqcrHkzjbqgVSXxsjYLqO+Kw8sTB1fEwLC8K
0Ka/92K1uYpRE3PVI7QOS+pbPVHLZYGH9HRcLS34gnQnojD+RrAWWMyqVEwMXJcq
zZEfeng1FAUYr4dPkKNzEcE3wrnAqMQpFCeu0r2JHMA0Ah+XdAnXxNdmhVTa3Qv6
BuydEvTeGEMD1chMzUHZTd0jXyyqkOjJpqwh3cWfiJTen+vMzDBQn3FyEQvdFDaa
mEUupYa2YQXNWqarSYjJokAXRdJ9fp067oFIwNfltEsfXoT8FkmGNBs1PnvsiLRp
FI0TzJAevP7F9LVYFNmfqwlkJul3HctjKv2Vg4h68AMrk+UCAwEAAaMhMB8wHQYD
VR0OBBYEFG9KighOWWVXffMRor4Js/Q5IpwRMA0GCSqGSIb3DQEBCwUAA4IBAQCT
OYyEV7XAOaWfjGwEWBlTRD7x/HhNM0QiXdOmZFZ1CHl3PwzgFOG0urihM91WqreZ
inbCKwOX/UnUjPc7bF4ighccZpHKv5GanmBXov0t0KuHjE4+6UWgVeZLjnNQZ8Hd
GiY+Z8mj5PTQpikLVXtJOyCxDt2GW3BvBfzxB6ZPr7JaD1He6aApbb0WogjZh0M4
dk31Xixy+wCuYSVrrrh4iscHEkufySkliKk3sseZMmUb1iyUtlTxIbJebpD1Yal9
Qm8TCXI4vuslh4bi6HVBNHrlWf6J7ndCHV/barQ6uWVJtO7QAAgqLrFc8JP6Yxiv
h19yWioGyQYXBkDDSSgM
-----END CERTIFICATE-----

我们只需要其中的public key部分,即:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtNIqpyseTONuqBVJfGyN
guo74rDyxMHV8TAsLwrQpr/3YrW5ilETc9UjtA5L6ls9UctlgYf0dFwtLfiCdCei
MP5GsBZYzKpUTAxcly7NkR96eDUUBRivh0+Qo3MRwTfCucCoxCkUJ67SvYkcwDQC
H5d0CdfE12aFVNrdC/oG7J0S9N4YQwPVyEzNQdlN3SNfLKqQ6MmmrCHdxZ+IlN6f
68zMMFCfcXIRC90UNpqYRS6lhrZhBc1apqtJiMmiQBdF0n1+nTrugUjA1+W0Sx9e
hPwWSYY0GzU+e+yItGkUjRPMkB68/sX0tVgU2Z+rCWQm6Xcdy2Mq/ZWDiHrwAyuT
5QIDAQAB
-----END PUBLIC KEY-----

放到新建的public.txt文件中。
然后把jwt.jks放到auth项目的resources目录下,public.txt放到client项目的resources目录下。

修改auth项目的授权服务配置文件

package xyz.liuzhuoming.auth.configurer;

import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

/**
* 授权服务配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableAuthorizationServer
@Slf4j
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
//token生成器设置为jwt生成器
.accessTokenConverter(jwtAccessTokenConverter)
.tokenServices(tokenServices());
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}

@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
}

然后新建JWT配置文件:

package xyz.liuzhuoming.auth.configurer;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

/**
* jwt配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
public class JwtConfigurer {

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
//配置密钥库资源路径及密钥库密码
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray());
//配置密钥别名
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return accessTokenConverter;
}

@Bean
public TokenStore tokenStore() {
//配置tokenStore为jwt
return new JwtTokenStore(jwtAccessTokenConverter());
}
}

修改client项目的授权客户端配置文件

新增JWT配置文件:

package xyz.liuzhuoming.client.config;

import java.io.IOException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.util.FileCopyUtils;

/**
* jwt配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
public class JwtConfigurer {

@Bean
protected JwtAccessTokenConverter accessTokenConverter() throws IOException {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//配置公钥资源路径
Resource resource = new ClassPathResource("public.txt");
String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
converter.setVerifierKey(publicKey);
return converter;
}
}

重启auth和client项目,用Postman请求http://localhost:5432/auth/oauth/token?username=user_2&password=123456&grant_type=password&scope=all,并在Postman的Authorization参数选择Basic Auth参数,并填写oauth2客户端id和密钥,请求之后返回结果:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3Mjc1MTIsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYTpwb3N0IiwiYjpnZXQiLCJhOmdldCJdLCJqdGkiOiI0ZGI5NDVlMy04YWUyLTRiNmMtYWRmNi0zODM0MmUzNDQ4YWUiLCJjbGllbnRfaWQiOiJjbGllbnRfMiIsInNjb3BlIjpbInNlcnZlciJdfQ.OdeHQuS1IS_QTlYolBPPthXM3Y__X4i3f9r6Vh9BjMyiG_h6H8_wKERCRJuuWOAkIOYtOdwNXcYzpfq324IiZJlxUEQcwc3N9hQ_6xvJCyl2cvtv7RczQIZ8i1XcB2aWWY4jfYtFBgdrmS8zbYxWNghIKjiJPbuCE4uWyG52huNpRAC7K8G0tjO9CyEx6y0Q5Yckonb4AH9sr9ZMflr7mcp0wNVIwzsbj6YwwLfBybZiEwEKWOhpJJM8cp3FRosFzUN0amzoMmohTd5GW--Tdo3yf-_Z3WbMo0pXSSMeQwry7bDpzblv6IfkuwjNSr8SHFckcoj2xxYdog54vgG6dg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInNlcnZlciJdLCJhdGkiOiI0ZGI5NDVlMy04YWUyLTRiNmMtYWRmNi0zODM0MmUzNDQ4YWUiLCJleHAiOjE1NzQzMTc3MTIsImF1dGhvcml0aWVzIjpbImE6cG9zdCIsImI6Z2V0IiwiYTpnZXQiXSwianRpIjoiMDE2MGU3ODItYzUwYS00ZjhkLTgzY2YtM2EyNGE5YWJiNWQzIiwiY2xpZW50X2lkIjoiY2xpZW50XzIifQ.TVfQciMDlyDRU_aVU4ENyAbp9Xy5T5IlkSJaPJ8D1Bpaq4SCxoMsVehvPOLSsMm2Z03UDn7td0f9M2dfiHhT2QLhWcjZo7n2KBKNm3h0zpB-3Qyf-sfo_FB0B9tgfJ4wCLhcvZalPlFtRXJNPVrTpRRsZW-e-BYoHqIxuId_B_vPkzlB-313DoWSRa93Sn39tTnsKQaemBdkD7irZOSa9YE-O8pMNADxOD0ci38ya_xtiloVveXcVSiuWKbcblh-61JcYT-780xLa3KEqdPBTfTcj50nD9Is9RTQ2B8SCFQrmsTrjnm7QmXfe0f9SphGxKBW0M_9p6OaFRPFZ9eL4g",
"expires_in": 1799,
"scope": "server",
"jti": "4db945e3-8ae2-4b6c-adf6-38342e3448ae"
}

可以看出access_token已经是标准的JWT格式。
用Postman请求http://localhost:5432/client/b,并在Postman的Authorization参数选择Bearer Token参数,并填写刚才获得的access_token,获得结果:

b

也就是说JWT作为Oauth2的token配置成功。
有兴趣的话可以在gateway添加简单的JWT有效性校验(有效期等),可以筛选掉部分非法请求,缺点就是因为JWT有效性自包含在token串内,所以无法主动注销token,只能等待自动过期。