Springboot 系列 (9) - Springboot+OAuth2(四) | 搭建独立资源服务器
在 “ Spring 系列 (6) - Springboot+OAuth2(一) | 使用 Security 搭建基于内存验证的授权服务器 ” 里的项目 SpringbootExample06 完成了一个基于内存验证的授权服务器。
本文将搭建一个独立资源服务器,并使用 SpringbootExample06 来测试该独立资源服务器。
1. 开发环境
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体系结构和开发环境配置 ”。
2. 创建 Spring Boot 基础项目
项目实例名称:SpringbootExample09
Spring Boot 版本:2.6.6
创建步骤:
(1) 创建 Maven 项目实例 SpringbootExample09;
(2) Spring Boot Web 配置;
(3) 导入 Thymeleaf 依赖包;
(4) 配置 jQuery;
具体操作请参考 “Springboot 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。
SpringbootExample09 和 SpringbootExample02 相比,SpringbootExample09 不配置 Bootstrap、模版文件(templates/*.html)和国际化。
3. 导入 OAuth2 依赖包
修改 pom.xml
1 <project ... > 2 ... 3 <dependencies> 4 ... 5 6 <!-- OAuth2 --> 7 <dependency> 8 <groupId>org.springframework.security.oauth</groupId> 9 <artifactId>spring-security-oauth2</artifactId> 10 <version>2.3.4.RELEASE</version> 11 </dependency> 12 13 ... 14 </dependencies> 15 16 ... 17 </project>
在IDE中项目列表 -> SpringbootExample09 -> 点击鼠标右键 -> Maven -> Reload Project
4. 配置 Resource Server
1) 修改 src/main/resources/application.properties 文件,修改后配置如下:
spring.main.banner-mode=off # Web server server.display-name=SpringbootExample09-Test server.address=localhost server.port=8888 # security spring.security.user.name=admin spring.security.user.password=123456 spring.security.user.roles=admin # oauth2 security.oauth2.client.client-id=3 security.oauth2.client.client-secret=2lo2ijxJ3e security.oauth2.resource.token-info-uri=http://localhost:9090/oauth/check_token
注:security 部分是配置 Oauth2.0 的默认登录用户。oauth2 部分是配置授权服务器(SpringbootExample06 项目)的参数用于 token 的远程验证。
2) 创建 src/main/java/com/example/config/ResourceServerConfig.java 文件
1 package com.example.config; 2 3 import org.springframework.beans.factory.annotation.Value; 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.RemoteTokenServices; 11 12 @Configuration 13 @EnableResourceServer 14 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 15 16 @Value("${security.oauth2.resource.token-info-uri}") 17 private String tokenInfoUri; 18 @Value("${security.oauth2.client.client-id}") 19 private String clientId; 20 @Value("${security.oauth2.client.client-secret}") 21 private String clientSecret; 22 23 @Override 24 public void configure(ResourceServerSecurityConfigurer resources) throws Exception { 25 26 RemoteTokenServices remoteTokenServices = new RemoteTokenServices(); 27 remoteTokenServices.setCheckTokenEndpointUrl(tokenInfoUri); 28 remoteTokenServices.setClientId(clientId); 29 remoteTokenServices.setClientSecret(clientSecret); 30 31 resources.tokenServices(remoteTokenServices); 32 } 33 34 @Override 35 public void configure(HttpSecurity http) throws Exception { 36 /* 37 注意: 38 39 1. 必须先加上: .requestMatchers().antMatchers(...),表示对资源进行保护,也就是说,在访问前要进行OAuth认证。 40 2. 接着:访问受保护的资源时,要具有相关权限。 41 42 否则,请求只是被 Security 的拦截器拦截,请求根本到不了 OAuth2 的拦截器。 43 44 requestMatchers() 部分说明: 45 46 mvcMatcher(String)}, requestMatchers(), antMatcher(String), regexMatcher(String), and requestMatcher(RequestMatcher). 47 */ 48 49 http 50 // Since we want the protected resources to be accessible in the UI as well we need 51 // session creation to be allowed (it's disabled by default in 2.0.6) 52 // 如果不设置 session,那么通过浏览器访问被保护的任何资源时,每次是不同的 SessionID,并且将每次请求的历史都记录在 OAuth2Authentication 的 details 中 53 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) 54 .and() 55 .requestMatchers() 56 .antMatchers("/api/**","/private/**") 57 .and() 58 .authorizeRequests() 59 .antMatchers("/api/**","/private/**") 60 .authenticated(); 61 } 62 63 }
注:/private/** 对应 src/main/resources/static/private 目录及其子目录,/api/** 对应 API 接口。
5. 测试实例 (Web 模式)
1) 创建 src/main/resources/templates/get_res.html 文件
1 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 2 <head> 3 <meta charset="UTF-8"> 4 <title>Get Resource</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <p><strong>Get Resource from Resource Server</strong></p> 10 11 <p>Access Token: <br><input type="text" name="access_token" id="access_token" style="width: 50%;" value="" /></p> 12 <p>Resources: <br> 13 <select id="resource_url" name="resource_url" style="width: 50%;"> 14 <option value=""></option> 15 <option th:value="@{/api/v1/user}" value="/api/v1/user" th:text="'API Resource -' + @{/api/v1/user}">API Resource</option> 16 <option th:value="@{/private/res.html}" value="/private/res.html" th:text="'HTML Resource -' + @{/private/res.html}">HTML Resource</option> 17 </select> 18 </p> 19 <p><button type="button" id="btn_get_res" class="btn btn-default">Get Resource</button></p> 20 21 <p><textarea name="res_result" id="res_result" class="form-control" style="width: 50%;" rows="5"></textarea></p> 22 23 24 <script type="text/javascript"> 25 26 $(document).ready(function() { 27 28 $('#btn_get_res').click(function() { 29 30 var access_token = $('#access_token').val(); 31 if (access_token == '') { 32 alert('Please enter access token'); 33 $('#access_token').focus(); 34 return; 35 } 36 37 var resource_url = $('#resource_url').val(); 38 if (resource_url == '') { 39 alert('Please select resource url'); 40 $('#resource_url').focus(); 41 return; 42 } 43 44 $('#res_result').val('Get resource ...'); 45 46 $.ajax({ 47 type: 'GET', 48 url: resource_url, 49 headers: { 'Authorization': 'Bearer ' + access_token }, 50 success: function(response) { 51 52 console.log(response); 53 $('#res_result').val(JSON.stringify(response)); 54 55 }, 56 error: function(err) { 57 58 console.log(err); 59 $('#res_result').val('Error: AJAX issue'); 60 61 } 62 }); 63 64 }); 65 }); 66 67 </script> 68 </body> 69 </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:8888/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>
注:上文 ResourceServerConfig 配置了 /private 目录及其子目录处于保护状态,需要 access token 才能访问。
3) 修改 src/main/java/com/example/controller/IndexController.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.ResponseBody; 6 7 @Controller 8 public class IndexController { 9 @ResponseBody 10 @RequestMapping("/test") 11 public String test() { 12 return "Test Page"; 13 } 14 15 @ResponseBody 16 @RequestMapping("/api/v1/user") 17 public String user() { 18 return "API v1 user"; 19 } 20 21 @RequestMapping("/get/resource") 22 public String getRes() { 23 return "get_res"; 24 } 25 }
访问 http://localhost:8888/api/v1/user,也会显示处于保护状态。
6. 运行
1) 运行授权服务器(SpringbootExample06)
访问 http://localhost:9090/oauth/test/client,,点击 Get Token 按钮,会在按钮下方显示如下JSON 格式数据:
{"access_token":"91fae539-6d24-4075-8a39-4eb15d197645","token_type":"bearer","expires_in":43199,"scope":"All"}
这里 91fae539-6d24-4075-8a39-4eb15d197645 就是我们所需要的 Access token。
2) 访问被保护资源 (Resource Server)
访问 http://localhost:8888/get/resource,使用上面取到的 Access Token 访问资源。
--------------------------------------
示例代码:https://gitee.com/slksm/public-codes/tree/master/demos/springboot-series/SpringbootExample09