Spring 系列 (6) - 在 Spring Boot 项目里使用 Security 和 OAuth2 搭建授权服务器(一)
作者:互联网
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Boot 给 Spring Security 提供了自动化配置方案 (spring-boot-starter-security),可以零配置使用 Spring Security。
OAuth 2.0 是 OAuth 协议的延续版本,但不向前兼容 OAuth 1.0 (即完全废止了 OAuth 1.0)。OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
比如:一个网站,允许用户不注册为该网站用户,而是使用微信授权后登录该网站。微信在这个流程中就处于 OAuth2 server(授权服务器)的位置,这个网站处于 OAuth client,这个网站(OAuth client)和微信(OAuth server)属于信任关系,带着微信授权令牌的用户,也被这个网站信任。
1. OAuth 简介
1) OAuth 基本概念
(1) Third-party Application:第三方应用程序,本文中又称 "客户端"(client);
(2) HTTP Service:HTTP 服务提供商,本文中简称 "服务提供商";
(3) Resource Owner:资源所有者,本文中又称 "用户"(user);
(4) User Agent:用户代理,本文中就是指浏览器;
(5) Authorization Server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6) Resource Server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
OAuth 在 "客户端"与 "服务提供商" 之间,设置了一个授权层(Authorization Layer)。"客户端" 不能直接登录 "服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端" 登录授权层所用的令牌(Token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端" 登录授权层以后,"服务提供商" 根据令牌的权限范围和有效期,向 "客户端" 开放用户储存的资料。
2) 授权模式
授权码模式(Authorization Code)、简化模式(Implicit)、密码模式(Resource Owner Password Credentials)、客户端模式(Client Credentials)
(1) 授权码模式(Authorization Code)
a) 用户访问客户端,后者将前者导向认证服务器;
b) 用户选择是否给予客户端授权;
c) 假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(Redirection URI),同时附上一个授权码;
d) 客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见;
e) 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(Access Token)和更新令牌(Refresh Token);
注:授权码模式是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与 "服务提供商" 的认证服务器进行互动。
(2) 简化模式(Implicit)
a) 用户决定是否给于客户端授权;
b) 假设用户给予授权,认证服务器将用户导向客户端指定的 "重定向 URI",并在 URI 的 Hash 部分包含了访问令牌;
c) 浏览器向资源服务器发出请求,其中不包括上一步收到的 Hash 值;
d) 资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌;
e) 浏览器执行上一步获得的脚本,提取出令牌;
f) 浏览器将令牌发给客户端;
注: 简化模式 (或称隐式模式),不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了 "授权码" 这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
(3) 密码模式(Resource Owner Password Credentials)
a) 用户向客户端提供用户名和密码;
b) 客户端将用户名和密码发给认证服务器,向后者请求令牌;
c) 认证服务器确认无误后,向客户端提供访问令牌;
注:密码模式,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向 "服务商提供商" 索要授权。
(4) 客户端模式(Client Credentials)
a) 客户端向认证服务器进行身份认证,并要求一个访问令牌;
b) 认证服务器确认无误后,向客户端提供访问令牌;
注:客户端模式指客户端以自己的名义,而不是以用户的名义,向 "服务提供商" 进行认证。严格地说,客户端模式并不属于 OAuth 框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求 "服务提供商" 提供服务,其实不存在授权问题。
OAuth 的客户端凭证校验是通过 ClientDetailsService 来实现的,OAuth 默认为我们提供了InMemoryClientDetailsService (内存验证) 和 JdbcClientDetailsService(JDBC验证),也可以实现自定义的 ClientDetailsService (自定义验证)。
本文将搭建一个基于内存验证的授权服务器。
2. 开发环境
Windows版本:Windows 10 Home (20H2)
IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
Apache Maven (https://maven.apache.org/):3.8.1
注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。
3. 创建 Spring Boot 基础项目
项目实例名称:SpringbootExample06
Spring Boot 版本:2.6.6
创建步骤:
(1) 创建 Maven 项目实例 SpringbootExample06;
(2) Spring Boot Web 配置;
(3) 导入 Thymeleaf 依赖包;
(4) 配置 jQuery;
具体操作请参考 “Spring 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。
SpringbootExample06 和 SpringbootExample02 相比,SpringbootExample06 不配置 Bootstrap、模版文件(templates/*.html)和国际化。
4. 导入 Security、 OAuth2 依赖包
修改 pom.xml
1 <project ... > 2 ... 3 <dependencies> 4 ... 5 6 <!-- Spring security --> 7 <dependency> 8 <groupId>org.springframework.boot</groupId> 9 <artifactId>spring-boot-starter-security</artifactId> 10 </dependency> 11 <!-- OAuth2 --> 12 <dependency> 13 <groupId>org.springframework.security.oauth</groupId> 14 <artifactId>spring-security-oauth2</artifactId> 15 <version>2.3.4.RELEASE</version> 16 </dependency> 17 18 ... 19 </dependencies> 20 21 ... 22 </project>
在IDE中项目列表 -> SpringbootExample06 -> 点击鼠标右键 -> Maven -> Reload Project
5. 配置 Security & OAuth2 (基于内存验证)
1) 创建 src/main/java/com/example/config/WebSecurityConfig.java 文件
1 package com.example.config; 2 3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 6 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 import org.springframework.security.authentication.AuthenticationManager; 9 10 @Configuration 11 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 12 13 @Override 14 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 15 // 内存模式 16 auth.inMemoryAuthentication() 17 .withUser("admin").password("{noop}123456").roles("ROLE_ADMIN") 18 .and() 19 .withUser("user").password("{noop}123456").roles("ROLE_USER"); 20 } 21 22 @Override 23 protected void configure(HttpSecurity http) throws Exception { 24 // 配置认证 25 http.authorizeRequests() 26 .antMatchers("/error", "/lib/**", "/oauth/**").permitAll() 27 .anyRequest().authenticated() 28 .and() 29 .formLogin() 30 .and() 31 .csrf().disable(); // 关闭 csrf 保护功能,默认是开启的 32 } 33 34 @Bean 35 public AuthenticationManager authenticationManagerBean() throws Exception { 36 return super.authenticationManagerBean(); 37 } 38 39 }
注:{noop} 表示 NoOpPasswordEncoder,{noop} 配合没有加过密的密码使用;{bcrypt} 表示 BCryptPasswordEncoder,{bcrypt} 配合加密过的密码使用;其它加密方式,参考两者的命名方式用小写字母。
这里定义的 AuthenticationManager 是 Spring Bean,在 AuthorizationServerConfig 里会装配使用。
2) 创建 src/main/java/com/example/config/oauth2/AuthorizationServerConfig.java 文件
1 package com.example.config.oauth2; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 6 import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 7 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 8 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 9 import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 10 import org.springframework.security.authentication.AuthenticationManager; 11 12 @Configuration 13 @EnableAuthorizationServer 14 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 15 @Autowired 16 private AuthenticationManager authenticationManager; 17 18 @Override 19 public void configure(AuthorizationServerSecurityConfigurer authServer) { 20 // 访问权限控制 21 authServer.tokenKeyAccess("permitAll()") 22 .checkTokenAccess("isAuthenticated()") 23 .allowFormAuthenticationForClients(); 24 } 25 26 @Override 27 public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 28 29 // 使内存模式 30 clients.inMemory().withClient("1") 31 .secret("{noop}4eti4hAaux") 32 .authorizedGrantTypes("authorization_code", "refresh_token") 33 .scopes("All") 34 .autoApprove(true) 35 .redirectUris("/oauth/test/code/callback") 36 .and() 37 .withClient("2") 38 .secret("{noop}xGJoD2i2lj") 39 .authorizedGrantTypes("implicit") 40 .scopes("All") 41 .autoApprove(true) 42 .redirectUris("/oauth/test/implicit/callback") 43 .and() 44 .withClient("3") 45 .secret("{noop}2lo2ijxJ3e") 46 .authorizedGrantTypes("password", "client_credentials") 47 .scopes("All") 48 .autoApprove(true); 49 } 50 51 @Override 52 public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 53 54 endpoints.authenticationManager(authenticationManager); 55 56 } 57 58 }
6. 测试实例 (Web 模式)
1) 授权码模式(Authorization Code)实例
(1) 创建 src/main/resources/templates/authorization_code.html 文件
1 <html> 2 <head> 3 <meta charset="UTF-8"> 4 <title th:text="${var}">Title</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <h4>OAuth 2.0 - Authorization Code</h4> 10 11 <p>Token URL: <br><input type="text" name="url" id="url" style="width: 50%;" th:value="@{/oauth/token}" value="" /></p> 12 <p>Client Id: <br><input type="text" name="client_id" id="client_id" style="width: 50%;" value="1" /></p> 13 <p>Client Secret: <br><input type="text" name="client_secret" id="client_secret" style="width: 50%;" value="4eti4hAaux" /></p> 14 <p>Code: <br><textarea name="code" id="code" rows="5" style="width: 50%;" readonly th:text="${code}"></textarea></p> 15 16 <p> </p> 17 <p> 18 <button type="button" id="btn_get_token" th:unless="${code}==null">Get Token</button> 19 <button type="button" id="btn_refresh_code" th:if="${code}==null">Refresh Code</button> 20 <br><br><textarea name="result" id="result" style="width: 50%;" rows="8"></textarea> 21 </p> 22 23 <p> </p> 24 <script type="text/javascript"> 25 26 var tokenObj = null; 27 28 $(document).ready(function(){ 29 30 $('#btn_get_token').click(function() { 31 32 var url = $('#url').val(); 33 if (url == '') { 34 alert('Please enter url'); 35 $('#url').focus(); 36 return; 37 } 38 39 var client_id = $('#client_id').val(); 40 if (client_id == '') { 41 alert('Please enter client id'); 42 $('#client_id').focus(); 43 return; 44 } 45 46 var client_secret = $('#client_secret').val(); 47 if (client_secret == '') { 48 alert('Please enter client secret'); 49 $('#client_secret').focus(); 50 return; 51 } 52 53 var code = $('#code').val(); 54 if (code == '') { 55 alert('Please enter code'); 56 $('#code').focus(); 57 return; 58 } 59 60 $('#result').val('Sending post ...'); 61 62 $.ajax({ 63 64 type: 'POST', 65 url: url, 66 data: { 67 grant_type: 'authorization_code', 68 client_id: client_id, 69 client_secret: client_secret, 70 redirect_uri: '/oauth/test/code/callback', 71 code: code, 72 }, 73 success: function(response) { 74 75 console.log(response); 76 $('#result').val(JSON.stringify(response)); 77 78 tokenObj = response; 79 80 }, 81 error: function() { 82 83 $('#result').val('Error: AJAX issue'); 84 85 tokenObj = null; 86 87 } 88 }); 89 90 }); 91 92 $('#btn_refresh_code').click(function() { 93 window.location.href = "/oauth/test/code"; 94 }); 95 96 }); 97 98 </script> 99 100 </body> 101 </html>
(2) 创建 src/main/java/com/example/controller/OauthController.java 文件
1 package com.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestParam; 6 import org.springframework.ui.Model; 7 8 @Controller 9 @RequestMapping("/oauth") 10 public class OauthController { 11 12 @RequestMapping("/test/code") 13 public String testCode() { 14 return "redirect:/oauth/authorize?client_id=1&redirect_uri=/oauth/test/code/callback&response_type=code"; 15 } 16 17 @RequestMapping("/test/code/callback") 18 public String testCodeCallback(@RequestParam String code, Model model) { 19 model.addAttribute("code", code); 20 return "authorization_code"; 21 } 22 }
运行并访问 http://localhost:9090/oauth/test/code,自动跳转到 http://localhost:9090/login (Spring security 的默认页面)。
输入上文 WebSecurityConfig 里配置的用户名/密码 (admin/123456),登录后跳转到 http://localhost:9090/test/code/callback,点击 Get Token 按钮,会在按钮下方显示如下数据:
{
"access_token": "0cea651f-4d01-43c0-9421-bc70d4eca081",
"token_type": "bearer",
"refresh_token": "a3418c82-5f80-4ee8-a1dd-9bdbfce52bdc",
"expires_in": 43199,
"scope": "All"
}
2) 简化模式(Implicit)实例
(1) 创建 src/main/resources/templates/implicit.html 文件
1 <html> 2 <head> 3 <meta charset="UTF-8"> 4 <title th:text="${var}">Title</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <h4>OAuth 2.0 - Implicit</h4> 10 11 <p id="message"></p> 12 13 <script type="text/javascript"> 14 15 function QueryStringToJSON(str) { 16 var pairs = str.split('&'); 17 var result = {}; 18 pairs.forEach(function (pair) { 19 pair = pair.split('='); 20 var name = pair[0] 21 var value = pair[1] 22 if (name.length) 23 if (result[name] !== undefined) { 24 if (!result[name].push) { 25 result[name] = [result[name]]; 26 } 27 result[name].push(value || ''); 28 } else { 29 result[name] = value || ''; 30 } 31 }); 32 return (result); 33 } 34 35 $(document).ready(function() { 36 37 var urlObj = window.location.href.split("#"); 38 if (urlObj.length > 1) { 39 40 var str = ''; 41 var obj = QueryStringToJSON(urlObj[1]); 42 for (k in obj) { 43 str += k + ': ' + obj[k] + '<br>'; 44 } 45 46 $('#message').html(str); 47 48 } 49 50 }); 51 52 </script> 53 </body> 54 </html>
(2) 修改 src/main/java/com/example/controller/OauthController.java 文件
1 package com.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestParam; 6 import org.springframework.ui.Model; 7 8 @Controller 9 @RequestMapping("/oauth") 10 public class OauthController { 11 12 ... 13 14 @RequestMapping("/test/implicit") 15 public String testImplicit() { 16 return "redirect:/oauth/authorize?client_id=2&client_secret=xGJoD2i2lj&grant_type=implicit&response_type=token"; 17 } 18 19 @RequestMapping("/test/implicit/callback") 20 public String testImplicitCallback(HttpServletRequest request) { 21 return "implicit"; 22 } 23 24 }
运行并访问 http://localhost:9090/oauth/test/implicit,自动跳转到 http://localhost:9090/login (Spring security 的默认页面)。
输入上文 WebSecurityConfig 里配置的用户名和密码登录,登录后跳转到 http://localhost:9090/test/implicit/callback#access_token=52856cfb-29ab-46dc-87af-06a7d025bb77&token_type=bearer&expires_in=40739&scope=All,解析 “#” 符号之后的数据,显示如下:
OAuth 2.0 - Implicit
access_token: 52856cfb-29ab-46dc-87af-06a7d025bb77
token_type: bearer
expires_in: 40739
scope: All
3) 密码模式(Resource Owner Password Credentials)实例
(1) 创建 src/main/resources/templates/password.html 文件
1 <html> 2 <head> 3 <meta charset="UTF-8"> 4 <title th:text="${var}">Title</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <h4>OAuth 2.0 - Password</h4> 10 11 <p>Token URL: <br><input type="text" name="url" id="url" style="width: 50%;" th:value="@{/oauth/token}" value="" /></p> 12 <p>Client Id: <br><input type="text" name="client_id" id="client_id" style="width: 50%;" value="3" /></p> 13 <p>Client Secret: <br><input type="text" name="client_secret" id="client_secret" style="width: 50%;" value="2lo2ijxJ3e" /></p> 14 <p>Username: <br><input type="text" name="username" id="username" style="width: 50%;" value="admin" /></p> 15 <p>Password: <br><input type="text" name="password" id="password" style="width: 50%;" value="123456" /></p> 16 17 <p> </p> 18 <p> 19 <button type="button" id="btn_get_token" >Get Token</button> 20 <br><br><textarea name="result" id="result" style="width: 50%;" rows="8"></textarea> 21 </p> 22 23 <script type="text/javascript"> 24 25 $(document).ready(function() { 26 27 $('#btn_get_token').click(function() { 28 29 var url = $('#url').val(); 30 if (url == '') { 31 alert('Please enter url'); 32 $('#url').focus(); 33 return; 34 } 35 36 var client_id = $('#client_id').val(); 37 if (client_id == '') { 38 alert('Please enter client id'); 39 $('#client_id').focus(); 40 return; 41 } 42 43 var client_secret = $('#client_secret').val(); 44 if (client_secret == '') { 45 alert('Please enter client secret'); 46 $('#client_secret').focus(); 47 return; 48 } 49 50 var username = $('#username').val(); 51 if (username == '') { 52 alert('Please enter username'); 53 $('#username').focus(); 54 return; 55 } 56 57 var password = $('#password').val(); 58 if (password == '') { 59 alert('Please enter password'); 60 $('#password').focus(); 61 return; 62 } 63 64 $('#result').val('Sending post ...'); 65 66 $.ajax({ 67 68 type: 'POST', 69 url: url, 70 data: { 71 grant_type: 'password', 72 client_id: client_id, 73 client_secret: client_secret, 74 username: username, 75 password: password, 76 }, 77 success: function(response) { 78 79 console.log(response); 80 $('#result').val(JSON.stringify(response)); 81 82 }, 83 error: function() { 84 85 $('#result').val('Error: AJAX issue'); 86 87 } 88 }); 89 90 }); 91 92 }); 93 94 </script> 95 </body> 96 </html>
(2) 修改 src/main/java/com/example/controller/OauthController.java 文件
1 package com.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestParam; 6 import org.springframework.ui.Model; 7 8 @Controller 9 @RequestMapping("/oauth") 10 public class OauthController { 11 12 ... 13 14 @RequestMapping("/test/password") 15 public String testPassword() { 16 return "password"; 17 } 18 19 }
运行并访问 http://localhost:9090/oauth/test/password ,点击 Get Token 按钮,会在按钮下方显示如下JSON 格式数据:
{"access_token":"00f15603-786e-46d1-a091-8169b3bb2e94","token_type":"bearer","expires_in":43200,"scope":"All"}
4) 客户端模式(Client Credentials)实例
(1) 创建 src/main/resources/templates/client.html 文件
1 <html> 2 <head> 3 <meta charset="UTF-8"> 4 <title th:text="${var}">Title</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <h4>OAuth 2.0 - Client</h4> 10 11 <p>Token URL: <br><input type="text" name="url" id="url" style="width: 50%;" th:value="@{/oauth/token}" value="" /></p> 12 <p>Client Id: <br><input type="text" name="client_id" id="client_id" style="width: 50%;" value="3" /></p> 13 <p>Client Secret: <br><input type="text" name="client_secret" id="client_secret" style="width: 50%;" value="2lo2ijxJ3e" /></p> 14 15 <p> </p> 16 <p> 17 <button type="button" id="btn_get_token" >Get Token</button> 18 <br><br><textarea name="result" id="result" style="width: 50%;" rows="8"></textarea> 19 </p> 20 21 <script type="text/javascript"> 22 23 $(document).ready(function() { 24 25 $('#btn_get_token').click(function() { 26 27 var url = $('#url').val(); 28 if (url == '') { 29 alert('Please enter url'); 30 $('#url').focus(); 31 return; 32 } 33 34 var client_id = $('#client_id').val(); 35 if (client_id == '') { 36 alert('Please enter client id'); 37 $('#client_id').focus(); 38 return; 39 } 40 41 var client_secret = $('#client_secret').val(); 42 if (client_secret == '') { 43 alert('Please enter client secret'); 44 $('#client_secret').focus(); 45 return; 46 } 47 48 $('#result').val('Sending post ...'); 49 50 $.ajax({ 51 52 type: 'POST', 53 url: url, 54 data: { 55 grant_type: 'client_credentials', 56 client_id: client_id, 57 client_secret: client_secret, 58 }, 59 success: function(response) { 60 61 console.log(response); 62 $('#result').val(JSON.stringify(response)); 63 64 }, 65 error: function() { 66 67 $('#result').val('Error: AJAX issue'); 68 69 } 70 }); 71 72 }); 73 74 }); 75 76 </script> 77 </body> 78 </html>
(2) 修改 src/main/java/com/example/controller/OauthController.java 文件
1 package com.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestParam; 6 import org.springframework.ui.Model; 7 8 @Controller 9 @RequestMapping("/oauth") 10 public class OauthController { 11 12 ... 13 14 @RequestMapping("/test/client") 15 public String testClient() { 16 return "client"; 17 } 18 19 }
运行并访问 http://localhost:9090/oauth/test/client ,点击 Get Token 按钮,会在按钮下方显示如下JSON 格式数据:
{"access_token":"91fae539-6d24-4075-8a39-4eb15d197645","token_type":"bearer","expires_in":43199,"scope":"All"}
标签:OAuth2,Spring,Boot,springframework,secret,client,import,org,客户端 来源: https://www.cnblogs.com/tkuang/p/16270624.html