Spring 系列 (8) - OAuth2(三) | Springboot+OAuth2+Security 搭建基于内存验证的授权服务器和资源服务器


在 “” 里的项目 SpringbootExample06 完成了一个基于内存验证的授权服务器。

本文将完全复制 SpringbootExample06 的代码和配置到新项目 SpringbootExample08,并在新项目 SpringbootExample08 的基础上修改代码和配置,搭建一个与授权服务器共存的资源服务器。


1. 配置 Security & OAuth2 (基于内存验证)

    1) Security 配置

        对应 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         import org.springframework.security.oauth2.provider.token.TokenStore;
10         import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
11 
12         @Configuration
13         public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
14 
15             @Override
16             protected void configure(AuthenticationManagerBuilder auth) throws Exception {
17 
18                 auth.inMemoryAuthentication()
19                         .withUser("admin").password("{noop}123456").roles("ADMIN")
20                         .and()
21                         .withUser("user").password("{noop}123456").roles("USER");
22             }
23 
24             @Override
25             protected void configure(HttpSecurity http) throws Exception {
26                 // 配置认证
27                 http.authorizeRequests()
28                         .antMatchers("/error", "/lib/**", "/oauth/**").permitAll()
29                         .anyRequest().authenticated()
30                         .and()
31                         .formLogin()
32                         .and()
33                         .csrf().disable(); // 关闭 csrf 保护功能,默认是开启的
34             }
35 
36             @Bean
37             public AuthenticationManager authenticationManagerBean() throws Exception {
38                 return super.authenticationManagerBean();
39             }
40 
41             @Bean
42             public TokenStore tokenStoreBean() {
43                 // token保存在内存中(也可以保存在数据库、Redis中)。
44                 // 如果保存在中间件(数据库、Redis),那么资源服务器与认证服务器可以不在同一个工程中。
45                 // 注意:如果不保存 access_token,则没法通过 access_token 取得用户信息
46                 return new InMemoryTokenStore();
47             }
48         } 


    2) Authorization Server 配置

        对应 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         import org.springframework.security.oauth2.provider.token.TokenStore;
12 
13         @Configuration
14         @EnableAuthorizationServer
15         public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter  {
16             @Autowired
17             private AuthenticationManager authenticationManager;
18             @Autowired
19             private TokenStore tokenStore;
20 
21             @Override
22             public void configure(AuthorizationServerSecurityConfigurer authServer) {
23                 // 访问权限控制
24                 authServer.tokenKeyAccess("permitAll()")
25                         .checkTokenAccess("isAuthenticated()")
26                         .allowFormAuthenticationForClients();
27             }
28 
29             @Override
30             public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
31 
32                 // 使内存模式
33                 clients.inMemory().withClient("1")
34                                     .secret("{noop}4eti4hAaux")
35                                     .authorizedGrantTypes("authorization_code", "refresh_token")
36                                     .scopes("All")
37                                     //.resourceIds("rids_1")
38                                     .autoApprove(true)
39                                     .redirectUris("/oauth/test/code/callback")
40                                     .and()
41                                     .withClient("2")
42                                     .secret("{noop}xGJoD2i2lj")
43                                     .authorizedGrantTypes("implicit")
44                                     .scopes("All")
45                                     //.resourceIds("rids_2")
46                                     .autoApprove(true)
47                                     .redirectUris("/oauth/test/implicit/callback")
48                                     .and()
49                                     .withClient("3")
50                                     .secret("{noop}2lo2ijxJ3e")
51                                     .authorizedGrantTypes("password", "client_credentials")
52                                     .scopes("All")
53                                     //.resourceIds("rids_3")
54                                     .autoApprove(true);
55             }
56 
57             @Override
58             public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
59 
60                 endpoints.authenticationManager(authenticationManager);
61                 endpoints.tokenStore(tokenStore);
62 
63             }
64 
65         }


        注:withClient() 设置的 client_id 和 resourceIds() 设置的 resource_id 之间有约束关系,即 client 访问资源时,如果 Client 和 Resource Server 都设置了 resource Id,就会比对 resource Id。

    3) Resource Server 配置
    
        对应 src/main/java/com/example/config/oauth2/ResourceServerConfig.java 文件

 1         package com.example.config.oauth2;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4         import org.springframework.context.annotation.Configuration;
 5         import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 6         import org.springframework.security.config.http.SessionCreationPolicy;
 7         import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
 8         import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
 9         import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
10         import org.springframework.security.oauth2.provider.token.TokenStore;
11 
12         @Configuration
13         @EnableResourceServer
14         public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
15 
16             @Autowired
17             private TokenStore tokenStore;
18 
19             @Override
20             public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
21 
22                 resources.tokenStore(tokenStore);
23                 //resources.tokenStore(tokenStore).resourceId("rids_1");
24             }
25 
26             @Override
27             public void configure(HttpSecurity http) throws Exception {
28                 /*
29                     注意:
30 
31                     1. 必须先加上: .requestMatchers().antMatchers(...),表示对资源进行保护,也就是说,在访问前要进行OAuth认证。
32                     2. 接着:访问受保护的资源时,要具有相关权限。
33                     
34                     否则,请求只是被 Security 的拦截器拦截,请求根本到不了 OAuth2 的拦截器。
35 
36                     requestMatchers() 部分说明:
37 
38                         mvcMatcher(String)}, requestMatchers(), antMatcher(String), regexMatcher(String), and requestMatcher(RequestMatcher).
39                 */
40 
41                 http
42                     // Since we want the protected resources to be accessible in the UI as well we need
43                     // session creation to be allowed (it's disabled by default in 2.0.6)
44                     // 如果不设置 session,那么通过浏览器访问被保护的任何资源时,每次是不同的 SessionID,并且将每次请求的历史都记录在 OAuth2Authentication 的 details 中
45                     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
46                     .and()
47                     .requestMatchers()
48                     .antMatchers("/user","/private/**")
49                     .and()
50                     .authorizeRequests()
51                     .antMatchers("/user","/private/**")
52                     .authenticated();
53             }
54         }


        注:/private/** 对应 src/main/resources/static/private 目录及其子目录,/user 对应一个 JSON 接口。


2. 测试实例 (Web 模式)

    1) 授权码模式(Authorization Code)实例

        (1) 创建 src/main/resources/templates/authorization_code.html 文件

  1             <html lang="en" xmlns:th="http://www.thymeleaf.org">
  2             <head>
  3                 <meta charset="UTF-8">
  4                 <title th:text="${var}">Titletitle>
  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 Codeh4>
 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>
 17                     <button type="button" id="btn_get_token" th:unless="${code}==null">Get Tokenbutton>
 18                     <button type="button" id="btn_refresh_code" th:if="${code}==null">Refresh Codebutton>
 19                     <br><br><textarea name="result" id="result" style="width: 50%;" rows="8">textarea>
 20                 p>
 21 
 22                 <p id="user_area" style="display: none;">
 23                     <button type="button" id="btn_get_user" class="btn btn-default">Get User Infobutton>
 24                     <br><br><textarea name="user" id="user" class="form-control" style="width: 50%;"  rows="5">textarea>
 25                 p>
 26 
 27                 <p> p>
 28                 <script type="text/javascript">
 29 
 30                     var tokenObj = null;
 31 
 32                     $(document).ready(function(){
 33 
 34                         $('#btn_get_token').click(function() {
 35 
 36                             var url = $('#url').val();
 37                             if (url == '') {
 38                                 alert('Please enter url');
 39                                 $('#url').focus();
 40                                 return;
 41                             }
 42 
 43                             var client_id = $('#client_id').val();
 44                             if (client_id == '') {
 45                                 alert('Please enter client id');
 46                                 $('#client_id').focus();
 47                                 return;
 48                             }
 49 
 50                             var client_secret = $('#client_secret').val();
 51                             if (client_secret == '') {
 52                                 alert('Please enter client secret');
 53                                 $('#client_secret').focus();
 54                                 return;
 55                             }
 56 
 57                             var code = $('#code').val();
 58                             if (code == '') {
 59                                 alert('Please enter code');
 60                                 $('#code').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: 'authorization_code',
 72                                     client_id: client_id,
 73                                     client_secret: client_secret,
 74                                     redirect_uri: '/oauth/test/code/callback',
 75                                     code: code,
 76                                 },
 77                                 success: function(response) {
 78 
 79                                     console.log(response);
 80                                     $('#result').val(JSON.stringify(response));
 81 
 82                                     tokenObj = response;
 83                                     $('#user_area').css('display', '');
 84                                 },
 85                                 error: function() {
 86 
 87                                     $('#result').val('Error: AJAX issue');
 88 
 89                                     tokenObj = null;
 90                                     $('#user_area').css('display', 'none');
 91                                 }
 92                             });
 93 
 94                         });
 95 
 96                         $('#btn_get_user').click(function() {
 97 
 98                             if (tokenObj) {
 99 
100                                 $('#user').val('Get user info ...');
101 
102                                 $.ajax({
103                                     type: 'GET',
104                                     url: "/user",
105                                     headers: { 'Authorization': 'Bearer ' + tokenObj.access_token },
106                                     success: function(response) {
107 
108                                         console.log(response);
109                                         $('#user').val(JSON.stringify(response));
110 
111                                     },
112                                     error: function(err) {
113 
114                                         console.log(err);
115                                         $('#user').val('Error: AJAX issue');
116 
117                                     }
118                                 });
119                             } else {
120 
121                                 $('#user').val('Error: empty token object');
122                             }
123 
124                         });
125 
126                         $('#btn_refresh_code').click(function() {
127                             window.location.href = "/oauth/test/code";
128                         });
129 
130                     });
131 
132                 script>
133 
134             body>
135             html>


        (2) 创建 src/main/resources/static/private/res.html 文件

 1             <html>
 2             <head>
 3                 <meta charset="UTF-8">
 4                 <title>Titletitle>
 5             head>
 6             <body>
 7 
 8                 <h4>OAuth 2.0 - Resource Serverh4>
 9                 <p>Resource Page (HTML)p>
10 
11             body>
12             html> 


            访问 http://localhost:9090/private/res.html,会显示如下内容:

                This XML file does not appear to have any style information associated with it. The document tree is shown below.
               
                    Full authentication is required to access this resource
                    unauthorized
               


        (3) 修改 src/main/java/com/example/controller/IndexController.java 文件

 1             package com.example.controller;
 2 
 3             import java.security.Principal;
 4             import org.springframework.stereotype.Controller;
 5             import org.springframework.web.bind.annotation.RequestMapping;
 6             import org.springframework.web.bind.annotation.ResponseBody;
 7 
 8             @Controller
 9             public class IndexController {
10                 @ResponseBody
11                 @RequestMapping("/test")
12                 public String test() {
13                     return "Test Page";
14                 }
15 
16                 @RequestMapping("/user")
17                 @ResponseBody
18                 public Principal getUser(Principal principal) {
19                     // principal 被 security 拦截后,是 org.springframework.security.authentication.UsernamePasswordAuthenticationToken
20                     // 被 OAuth2 拦截后,是 OAuth2Authentication
21                     return principal;
22                 }
23             }


        (4) 创建 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 里配置的用户名和密码登录,登录后跳转到 http://localhost:9090/test/code/callback,点击 Get Token 按钮,会在按钮下方显示如下数据和 Get User Info 按钮:

                {
                    "access_token": "0cea651f-4d01-43c0-9421-bc70d4eca081",
                    "token_type": "bearer",
                    "refresh_token": "a3418c82-5f80-4ee8-a1dd-9bdbfce52bdc",
                    "expires_in": 43199,
                    "scope": "All"
                }

            点击 Get User Info 按钮,会在按钮下方显示如下数据:

                {"authorities":[],"details":{"remoteAddress":"127.0.0.1","sessionId":"6E9C151500E869B04A5395E52A563288","tokenValue":"20bbe84b-0c70-4978-b6b2-24248ca2fb61","tokenType":"Bearer","decodedDetails":null},"authenticated":true,"userAuthentication":null,"oauth2Request":{"clientId":"3","scope":["All"],"requestParameters":{"grant_type":"client_credentials","client_id":"3"},"resourceIds":[],"authorities":[],"approved":true,"refresh":false,"redirectUri":null,"responseTypes":[],"extensions":{},"grantType":"client_credentials","refreshTokenRequest":null},"principal":"3","credentials":"","clientOnly":true,"name":"3"}
            
            访问 http://localhost:9090/private/res.html?access_token=0cea651f-4d01-43c0-9421-bc70d4eca081

                OAuth 2.0 - Resource Server
                Resource Page (HTML)

            链接上的 access_token 也可以放到 HTTP header 里(使用 Postman 或 AJAX 测试),格式如下:

                Authorization:bearer 0cea651f-4d01-43c0-9421-bc70d4eca081


    2) 简化模式(Implicit)实例

        略


    3) 密码模式(Resource Owner Password Credentials)实例

        略


    4) 客户端模式(Client Credentials)实例

        略


    注:以上省略的实例代码,请参考 “” 里的 “6. 测试实例 (Web 模式) ” 和本文的授权码模式(Authorization Code)实例。