前言
首先我们先来弄清楚这里的前后端分离指的是什么?我们上篇文章已经指出oauth2有四种角色分别是(客户端、授权服务端和资源服务端和资源所有者),资源服务端和资源所有者是指用户数据和用户自己,所以这里的前后端要么是客户端应用要么是授权服务端那么到底是哪个呢?因为授权服务端已经实现了登录和授权相关页面因此我们只需要改造一下即可,所以这里的前后端指的就是客户端应用了。
说到客户端应用一般有两种实现方式,一种是传统的前后台一体的单体架构项目如:jsp、asp.net等等,另一种是使用分布式架构的前后端分离技术,如React、Vue这样的前端框架做到前后台相互隔离。
还有一点我们这里实现的单点登录是使用授权码(authorization-code)模式不要搞晕最后全部实现了都不知道使用的是OAuth2的哪种模式就有点搞笑了。
不管是单体架构还是分布式架构,授权服务器都是一样的,所以我们先来构建授权服务器及相关代码实现来开始本章。
单体架构实现单点登录
单体架构上面已经提到了就是前后台一体的应用,后面会介绍分布式架构前后端分离应用实现单点登录,我们先从单体架构来实现单点登录来比较他们两者的区别,也请大家自行思考两者的优缺点并在实际项目中做出选择。
单体架构介绍
这个单体架构的单点登录系统包括下面几个模块:
awbeci-sso: 父模块
awbeci-sso-server: 认证和授权服务端(端口:8900)
awbeci-sso-client1: 单点登录客户端示例(端口:8901)
awbeci-sso-client2: 单点登录客户端示例(端口:8902)
构建授权服务端
首先,我想让初学者了解授权服务端的作用以及相关概念,授权服务端主要做这样几件事:
1. 授权服务端接受客户端的访问(废话)
2. 客户端向授权服务端发起请求令牌后,授权服务端首先会验证是哪个应用(client_id和client_secret),接着会验证是哪个用户(username和password)并要求用户授权。注意:这些参数和凭据都是客户端和用户给的
3. 授权服务端验证都通过了,就会根据客户端传的redicrect_uri并带着code跳转到该链接返回给客户端
4. 客户端带着原先传递的参数加上授权服务端给的code再次请求授权服务端,授权服务端接收并再次验证是哪个应用,哪个用户,哪个code通过一系列的验证通过之后就正式返回token给客户端。
现在知道了授权服务端到底做了哪些事,这样接下来我们要做的就是去实现这样一整套流程,如果后面有什么还是不清楚可以回头再来看看。
创建授权服务端项目
首先我们先来构建一个maven项目,名称为awbeci-sso-server并在pom.xml中添加相关依赖包,如下所示:
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>awbeci-sso-server</artifactId>
<groupId>org.awbeci</groupId>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency><!--热部署依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
接着新建相关Package并在下面添加SsoServerApplication类,如下所示:
@SpringBootApplication
public class SsoServerApplication {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class, args);
}
}
认证服务配置
首先添加添加SsoAuthorizationServerConfig类它是继承自AuthorizationServerConfigurerAdapter类,如下所示:
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客户端的client_id和client_secret并保存到内存中
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder.encode("123456"))
.redirectUris("http://127.0.0.1:8901/client1/login")
.authorizedGrantTypes("authorization_code")
.scopes("all")
.and()
.withClient("client2")
.secret(passwordEncoder.encode("123456"))
.redirectUris("http://127.0.0.1:8902/client2/login")
.authorizedGrantTypes("authorization_code")
.scopes("all");
}
}
注意:必须设置回调地址redirectUris
,并且格式是http://客户端IP:端口/login
的格式,否则会报OAuth Error error=”invalid_request”, error_description=”At least one redirect_uri must be registered with the client.”
安全配置
Spring Security 安全配置。在安全配置类里我们配置了:
- 配置请求URL的访问策略。
- 配置用户名密码信息从UserDetailsService中获取。
@Configuration
@EnableWebSecurity
public class SsoWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// 用来处理用户验证
// 被注入OAuth2Config类中的 endpoints方法中
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable().cors();
}
}
接下来配置UserDetailsService,代码如下所示:
public class SsoUserDetailsService implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟从数据库获取用户信息,实际项目中要从数据库中获取用户信息
return User.withUsername(username)
.password(passwordEncoder.encode("123456"))
.authorities("ROLE_ADMIN")
.build();
}
}
这样就全部配置好了认证和授权的服务端了,下面我们就来配置客户端。
构建客户端
创建授权客户端项目
客户端我们来新建两个maven项目,当然你也可以新建2个以上的,下面是新建的client1和client2的pom.xml依赖包,如下:
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>awbeci-sso-client2</artifactId>
<groupId>org.awbeci</groupId>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency><!--热部署依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
接着新建相关Package并在下面添加SsoServerApplication类,如下所示:
@SpringBootApplication
// 这里开启了单点登录访问
@EnableOAuth2Sso
public class SsoClient1Application {
public static void main(String[] args) {
SpringApplication.run(SsoClient1Application.class, args);
}
}
现在我们来配置application.yml并设置好客户端应用以及授权地址和获取token的地址
server:
port: 8901
servlet:
context-path: /client1
spring:
application:
name: client1service
thymeleaf:
cache: false
mode: HTML5
encoding: UTF-8
servlet:
content-type: text/html
prefix: classpath:/templates/
suffix: .html
resources:
chain:
strategy:
content:
enabled: true
paths: /**
security:
oauth2:
client:
client-id: client1
client-secret: 123456
user-authorization-uri: http://127.0.0.1:8900/sso/oauth/authorize
access-token-uri: http://127.0.0.1:8900/sso/oauth/token
resource:
token-info-uri: http://127.0.0.1:8900/sso/oauth/check_token
注意:上面配置中security作用就是当客户端发现没有身份认证的时候会自动跳转到http://127.0.0.1:8900/sso/oauth/authorize去认证用户,认证成功之后会返回到客户端,客户端就可以通过http://127.0.0.1:8900/sso/oauth/token去自动获取token,就不需要手动去跳转和获取了,这两个自动操作的过程中已经做了code的获取和返回,但是你是感觉不到的,这也是单体架构的优势了,而我们后面做的前后台分离项目就要手动去操作了。
新建客户端首页页面
我们会在client1和client2项目上新建首页页面,这个首页页面就是一个简单的html页面我们使用的是thymeleaf来制作这样的页面,我们会在客户端1(client1)和客户端2(client2)上面制作相同的这样的页面,目的:就是当不管访问哪个客户端页面当你已经认证了用户那么你相互跳转并访问就不需要再跑去验证用户了,但是如果你没通过验证那么不管你访问哪个页面就要去验证用户。就是模拟一个多应用下的验证和授权操作,如果你们实在想像不出来,可以想想淘宝和天猫登录的操作,我们就是模拟这样的一个操作过程。下面是客户端1和客户端2的首页代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>client1</title>
<meta charset="utf-8"/>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
<link type="text/css" rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/css/bootstrap.min.css">
<body>
<div>
访问<a href="http://127.0.0.1:8902/client2">客户端2</a>
</div>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/js/bootstrap.min.js"></script>
</body>
</html>
记得加上Controller
@RestController
public class TestController {
@GetMapping("/")
public ModelAndView index(){
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
}
客户端Client2的设置同上,这里就不详细讲了,大家可以看我的源码,下面我们把三个项目运行起来试试。
当我访问客户端1client1的时候会直接跳转到localhost:8900/sso/login,原因上面已经说过了,下面我们输入用户名和密码点击登录试试。
上面提示你要不要给应用client1授权访问,选择Approve(允许)再点击Authoriza(授权)即可,这样就会跳转到你返回过来的redirect_uri链接了。
现在就可以访问客户端1的页面了,中间已经做了code和token的验证了(上面已经说过了),用户是感受不到的。现在你点击客户端2,它会跳转到客户端2,因为我们已经验证了用户,所以跳转到客户端2是不用再验证用户了,我们点击试试。
虽然不用再验证用户了,但是这里还是要你授权,这里像客户端一样选择即可。
这样客户端2也能访问了,这样就完成了单体架构的单点登录了。不过上面的过程有几个问题需要我们去解决,我列举了一下:
1、自动授权
2、使用数据库保存客户端和token信息
3、使用jwt生成token
下面我们就来一个个改造,首先我们来改造下自动授权,不用每次去点击了。
改造1:自动授权
改造也很简单,我们改下SsoAuthorizationServerConfig的configure(ClientDetailsServiceConfigurer clients)方法,如下所示:
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder.encode("123456"))
.redirectUris("http://127.0.0.1:8901/client1/login")
.authorizedGrantTypes("authorization_code")
+ .autoApprove(true)
.scopes("all")
.and()
.withClient("client2")
.secret(passwordEncoder.encode("123456"))
.redirectUris("http://127.0.0.1:8902/client2/login")
.authorizedGrantTypes("authorization_code")
+ .autoApprove(true)
.scopes("all");
现在你再输入用户名和密码之后就不用点击授权了,就直接跳转到客户端了,大家可以试试。
下面我们来改造下代码使用数据库保存客户端信息。
改造2:客户端信息使用mysql
首先我们要建几个跟OAuth2相关的表来保存client和token相关信息,如下所示:
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` longblob,
`refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_approvals` (
`userId` varchar(255) DEFAULT NULL,
`clientId` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(255) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`authentication` varbinary(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication` longblob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
再添加client1和client2的客户端信息,如图所示:
上面的client的密码大家可以使用如下代码生成:
System.out.println(new BCryptPasswordEncoder().encode("123456"));
接着添加下jdbc和mysql的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
接着我们在application.yml里面添加对mysql的配置
spring:
datasource:
url: jdbc:mysql://your-mysql-domain/your-database?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false
username: your-username
password: your-password
driver-class-name: com.mysql.jdbc.Driver
jpa:
show-sql: true
接着改造下SsoAuthorizationServerConfig类代码
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
+ @Autowired
+ private DataSource dataSource;
+ @Autowired
+ private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()");
}
+ @Override
+ public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
+ endpoints
+ .userDetailsService(userDetailsService)
+ .tokenStore(tokenStore());
+ }
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客户端的client_id和client_secret并保存到内存中
- clients.inMemory()
- .withClient("client1")
- .secret(passwordEncoder.encode("123456"))
- .redirectUris("http://127.0.0.1:8901/client1/login")
- .authorizedGrantTypes("authorization_code")
- .autoApprove(true)
- .scopes("all")
- .and()
- .withClient("client2")
- .secret(passwordEncoder.encode("123456"))
- .redirectUris("http://127.0.0.1:8902/client2/login")
- .authorizedGrantTypes("authorization_code")
- .autoApprove(true)
- .scopes("all");
+ clients.jdbc(dataSource);
}
}
这样就完成了改造,现在我们再重新访问客户端1或者2,输入用户名和密码,再看看数据库oauth_access_token表里面的数据,如下所示:
改造3:使用jwt生成token
首先我们添加JWTTokenStoreConfig配置类:
@Configuration
public class JWTTokenStoreConfig {
// 设置TokenStore为JwtTokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
// 在jwt和oauth2服务器之间充当翻译
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 定义将用于签署令牌的签名密钥(自定义 存储在git上authentication.yml文件)
// jwt是不保密的,所以要另外加签名验证jwt token
// todo:最好不要写死,复杂点更好
converter.setSigningKey("awbeci");
return converter;
}
// 设置TokenEnhancer增强器中使用JWTTokenEnhancer增强器
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}
// @Primary作用:如果有多个特定类型bean那么就使用被@Primary标注的bean类型进行自动注入
// @Bean
// @Primary
// public DefaultTokenServices tokenServices() {
// // 用于从出示给服务的令牌中读取数据
// DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
// defaultTokenServices.setTokenStore(tokenStore());
// defaultTokenServices.setSupportRefreshToken(true);
// return defaultTokenServices;
// }
}
接着,我们添加jwt的增强器JWTTokenEnhancer类,作用:扩展jwt内容信息。
// jwt token 扩展器,加进自己的数据内容
public class JWTTokenEnhancer implements TokenEnhancer {
// 要进行增强需要覆盖enhance方法
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("enhancer_content", "there is enhancer content");
// 所有附加的属性都放到HashMap中,并设置在传入该方法的accessToken变量上
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
return oAuth2AccessToken;
}
}
接着,我们修改SsoAuthorizationServerConfig类来支持jwt
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
- @Autowired
- private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
- @Bean
- public TokenStore tokenStore() {
- return new JdbcTokenStore(dataSource);
- }
+ @Autowired
+ private AuthenticationManager authenticationManager;
+ @Autowired
+ private TokenStore tokenStore;
+ // 将JWTTokenStore类中的JwtAccessTokenConverter关联到OAUTH2
+ @Autowired
+ private JwtAccessTokenConverter jwtAccessTokenConverter;
+ // 自动将JWTTokenEnhancer装配到TokenEnhancer类中
+ // token增强类,需要添加额外信息内容的就用这个类
+ @Autowired
+ private TokenEnhancer jwtTokenEnhancer;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
+ // 设置jwt签名和jwt增强器到TokenEnhancerChain
+ TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
+ tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, +jwtAccessTokenConverter));
endpoints.tokenStore(tokenStore)
+ // 在jwt和oauth2服务器之间充当翻译(签名)
+ .accessTokenConverter(jwtAccessTokenConverter)
+ // 令牌增强器类:扩展jwt token
+ .tokenEnhancer(tokenEnhancerChain) //JWT
.userDetailsService(userDetailsService)
+ .authenticationManager(authenticationManager)
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
}
接着,我们再来改造下客户端1和2下的application.yml
security:
oauth2:
client:
client-id: client2
client-secret: 123456
user-authorization-uri: http://127.0.0.1:8900/sso/oauth/authorize
access-token-uri: http://127.0.0.1:8900/sso/oauth/token
+ resource:
+ jwt:
+ key-uri: http://127.0.0.1:8900/sso/oauth/token_key
- resource:
- token-info-uri: http://127.0.0.1:8900/sso/oauth/check_token
添加获取用户信息Controller
@SpringBootApplication
@EnableOAuth2Sso
public class SsoClient2Application {
+ @GetMapping("/user")
+ public Authentication user(Authentication user) {
+ return user;
+ }
public static void main(String[] args) {
SpringApplication.run(SsoClient2Application.class, args);
}
}
现在我们再重新登录试试,并试着访问/user。
至此,单体架构的单点登录就完成了,下节我们讲解分布式架构下前后端分离项目的单点登录。
参考:
Oauth2授权模式访问之授权码模式(authorization_code)访问
SpringCloud OAuth2实现单点登录以及OAuth2源码原理解析
Spring Security OAUTH2 获取用户信息
SpringBoot配置属性之Security
Spring Security OAuth2 配置注意点
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。