1. 程式人生 > 其它 >Spring 系列 (8) - 在 Spring Boot 專案裡使用 Security 和 OAuth2 搭建授權伺服器(三)

Spring 系列 (8) - 在 Spring Boot 專案裡使用 Security 和 OAuth2 搭建授權伺服器(三)

在 “Spring 系列 (6) - 在 Spring Boot 專案裡使用 Security 和 OAuth2 搭建授權伺服器(一)” 裡的專案 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/oauth/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>
  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>
 17                     <button type="button" id="btn_get_token" th:unless="${code}==null">Get Token</button>
 18                     <button type="button" id="btn_refresh_code" th:if="${code}==null">Refresh Code</button>
 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 Info</button>
 24                     <br><br><textarea name="user" id="user" class="form-control" style="width: 50%;"  rows="5"></textarea>
 25                 </p>
 26 
 27                 <p>&nbsp;</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>Title</title>
 5             </head>
 6             <body>
 7 
 8                 <h4>OAuth 2.0 - Resource Server</h4>
 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.
                <oauth>
                    <error_description>Full authentication is required to access this resource</error_description>
                    <error>unauthorized</error>
                </oauth>

        (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)例項

        略


    注:以上省略的例項程式碼,請參考 “Spring 系列 (6) - 在 Spring Boot 專案裡使用 Security 和 OAuth2 搭建授權伺服器(一)” 裡的 “6. 測試例項 (Web 模式) ” 和本文的授權碼模式(Authorization Code)例項。