Springboot 系列 (8) - Springboot+OAuth2(三) | 使用 Security 搭建基于内存验证的授权/资源服务器
在 “ Springboot 系列 (6) - 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}">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> </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)实例。
--------------------------------------
示例代码:https://gitee.com/slksm/public-codes/tree/master/demos/springboot-series/SpringbootExample08
浙公网安备 33010602011771号