Spring Security oAuth2 创建资源服务器

    技术2026-06-10  7

    创建资源服务器

    概述

    在 为什么需要 oAuth2 和 RBAC 基于角色的权限控制 章节,我们介绍过资源的概念,简单点说就是需要被访问的业务数据或是静态资源文件都可以被称作资源。

    为了让大家更好的理解资源服务器的概念,我们单独创建一个名为 hello-spring-security-oauth2-resource 资源服务器的项目,该项目的主要目的就是对数据表的 CRUD 操作,而这些操作就是对资源的操作了。

    操作流程

    初始化资源服务器数据库POM 所需依赖同认证服务器配置资源服务器配置资源(Controller)

    初始化资源服务器数据库

    CREATE TABLE `tb_content` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `category_id` bigint(20) NOT NULL COMMENT '内容类目ID', `title` varchar(200) DEFAULT NULL COMMENT '内容标题', `sub_title` varchar(100) DEFAULT NULL COMMENT '子标题', `title_desc` varchar(500) DEFAULT NULL COMMENT '标题描述', `url` varchar(500) DEFAULT NULL COMMENT '链接', `pic` varchar(300) DEFAULT NULL COMMENT '图片绝对路径', `pic2` varchar(300) DEFAULT NULL COMMENT '图片2', `content` text COMMENT '内容', `created` datetime DEFAULT NULL, `updated` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `category_id` (`category_id`), KEY `updated` (`updated`) ) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8; insert into `tb_content`(`id`,`category_id`,`title`,`sub_title`,`title_desc`,`url`,`pic`,`pic2`,`content`,`created`,`updated`) values (28,89,'标题','子标题','标题说明','http://www.jd.com',NULL,NULL,NULL,'2019-04-07 00:56:09','2019-04-07 00:56:11'), (29,89,'ad2','ad2','ad2','http://www.baidu.com',NULL,NULL,NULL,'2019-04-07 00:56:13','2019-04-07 00:56:15'), (30,89,'ad3','ad3','ad3','http://www.sina.com.cn',NULL,NULL,NULL,'2019-04-07 00:56:17','2019-04-07 00:56:19'), (31,89,'ad4','ad4','ad4','http://www.funtl.com',NULL,NULL,NULL,'2019-04-07 00:56:22','2019-04-07 00:56:25'); CREATE TABLE `tb_content_category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID', `parent_id` bigint(20) DEFAULT NULL COMMENT '父类目ID=0时,代表的是一级的类目', `name` varchar(50) DEFAULT NULL COMMENT '分类名称', `status` int(1) DEFAULT '1' COMMENT '状态。可选值:1(正常),2(删除)', `sort_order` int(4) DEFAULT NULL COMMENT '排列序号,表示同级类目的展现次序,如数值相等则按名称次序排列。取值范围:大于零的整数', `is_parent` tinyint(1) DEFAULT '1' COMMENT '该类目是否为父类目,1为true,0为false', `created` datetime DEFAULT NULL COMMENT '创建时间', `updated` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `parent_id` (`parent_id`,`status`) USING BTREE, KEY `sort_order` (`sort_order`) ) ENGINE=InnoDB AUTO_INCREMENT=98 DEFAULT CHARSET=utf8 COMMENT='内容分类'; insert into `tb_content_category`(`id`,`parent_id`,`name`,`status`,`sort_order`,`is_parent`,`created`,`updated`) values (30,0,'LeeShop',1,1,1,'2015-04-03 16:51:38','2015-04-03 16:51:40'), (86,30,'首页',1,1,1,'2015-06-07 15:36:07','2015-06-07 15:36:07'), (87,30,'列表页面',1,1,1,'2015-06-07 15:36:16','2015-06-07 15:36:16'), (88,30,'详细页面',1,1,1,'2015-06-07 15:36:27','2015-06-07 15:36:27'), (89,86,'大广告',1,1,0,'2015-06-07 15:36:38','2015-06-07 15:36:38'), (90,86,'小广告',1,1,0,'2015-06-07 15:36:45','2015-06-07 15:36:45'), (91,86,'商城快报',1,1,0,'2015-06-07 15:36:55','2015-06-07 15:36:55'), (92,87,'边栏广告',1,1,0,'2015-06-07 15:37:07','2015-06-07 15:37:07'), (93,87,'页头广告',1,1,0,'2015-06-07 15:37:17','2015-06-07 15:37:17'), (94,87,'页脚广告',1,1,0,'2015-06-07 15:37:31','2015-06-07 15:37:31'), (95,88,'边栏广告',1,1,0,'2015-06-07 15:37:56','2015-06-07 15:37:56'), (96,86,'中广告',1,1,1,'2015-07-25 18:58:52','2015-07-25 18:58:52'), (97,96,'中广告1',1,1,0,'2015-07-25 18:59:43','2015-07-25 18:59:43');

    POM

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>hello-spring-security-oauth2</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>hello-spring-security-oauth2-resource</artifactId> <licenses> <license> <name>Apache 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <!-- CP --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>${hikaricp.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <!-- 排除 tomcat-jdbc 以使用 HikariCP --> <exclusion> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.shane.spring.security.oauth2.resource.OAuth2ResourceApplication</mainClass> </configuration> </plugin> </plugins> </build> </project>

    application.yml

    spring: application: name: oauth2-resource datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.141.130:3306/oauth2_resource?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 hikari: minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 10 auto-commit: true pool-name: MyHikariCP max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 security: oauth2: client: client-id: client client-secret: secret access-token-uri: http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize resource: token-info-uri: http://localhost:8080/oauth/check_token server: port: 8081 servlet: context-path: /contents mybatis: type-aliases-package: com.shane.spring.security.oauth2.resource.domain mapper-locations: classpath:mapper/*.xml logging: level: root: INFO org.springframework.web: INFO org.springframework.security: INFO org.springframework.security.oauth2: INFO

    Application

    package com.shane.spring.security.oauth2.resource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan; @SpringBootApplication @MapperScan(basePackages = "com.shane.spring.security.oauth2.resource.mapper") public class OAuth2ResourceApplication { public static void main(String[] args) { SpringApplication.run(OAuth2ResourceApplication.class, args); } }

    关键步骤

    配置资源服务器

    创建一个类继承 ResourceServerConfigurerAdapter 并添加相关注解:

    @Configuration@EnableResourceServer:资源服务器@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true):全局方法拦截 package com.shane.spring.security.oauth2.resource.configure; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .exceptionHandling() .and() // Session 创建策略 // ALWAYS 总是创建 HttpSession // IF_REQUIRED Spring Security 只会在需要时创建一个 HttpSession // NEVER Spring Security 不会创建 HttpSession,但如果它已经存在,将可以使用 HttpSession // STATELESS Spring Security 永远不会创建 HttpSession,它不会使用 HttpSession 来获取 SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 以下为配置所需保护的资源路径及权限,需要与认证服务器配置的授权部分对应 .antMatchers("/").hasAuthority("SystemContent") .antMatchers("/view/**").hasAuthority("SystemContentView") .antMatchers("/insert/**").hasAuthority("SystemContentInsert") .antMatchers("/update/**").hasAuthority("SystemContentUpdate") .antMatchers("/delete/**").hasAuthority("SystemContentDelete"); } }

    数据传输对象

    创建一个名为 ResponseResult 的通用数据传输对象

    package com.shane.spring.security.oauth2.resource.dto; import lombok.Data; import java.io.Serializable; /** * 通用的返回对象 * * @param <T> */ @Data public class ResponseResult<T> implements Serializable { private static final long serialVersionUID = 3468352004150968551L; /** * 状态码 */ private Integer state; /** * 消息 */ private String message; /** * 返回对象 */ private T data; public ResponseResult() { super(); } public ResponseResult(Integer state) { super(); this.state = state; } public ResponseResult(Integer state, String message) { super(); this.state = state; this.message = message; } public ResponseResult(Integer state, Throwable throwable) { super(); this.state = state; this.message = throwable.getMessage(); } public ResponseResult(Integer state, T data) { super(); this.state = state; this.data = data; } public ResponseResult(Integer state, String message, T data) { super(); this.state = state; this.message = message; this.data = data; } public Integer getState() { return state; } public void setState(Integer state) { this.state = state; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((data == null) ? 0 : data.hashCode()); result = prime * result + ((message == null) ? 0 : message.hashCode()); result = prime * result + ((state == null) ? 0 : state.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ResponseResult<?> other = (ResponseResult<?>) obj; if (data == null) { if (other.data != null) { return false; } } else if (!data.equals(other.data)) { return false; } if (message == null) { if (other.message != null) { return false; } } else if (!message.equals(other.message)) { return false; } if (state == null) { if (other.state != null) { return false; } } else if (!state.equals(other.state)) { return false; } return true; } }

    Controller

    package com.shane.spring.security.oauth2.resource.controller; import com.shane.spring.security.oauth2.resource.domain.TbContent; import com.shane.spring.security.oauth2.resource.dto.ResponseResult; import com.shane.spring.security.oauth2.resource.service.TbContentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class TbContentController { @Autowired private TbContentService tbContentService; @GetMapping(value = "/") public ResponseResult<List<TbContent>> list() { List<TbContent> tbContents = tbContentService.selectAll(); return new ResponseResult<List<TbContent>>(HttpStatus.OK.value(), HttpStatus.OK.toString(), tbContents); } }

    访问资源

    访问获取授权码

    通过浏览器访问 http://localhost:8080/oauth/authorize?client_id=client&response_type=code 第一次访问会跳转到登录页面

    验证成功后会询问用户是否授权客户端

    选择授权后会跳转到百度,浏览器地址上还会包含一个授权码(code=1JuO6V),浏览器地址栏会显示如下地址: https://www.baidu.com/?code=1JuO6V

    向服务器申请令牌

    通过 CURL 或是 Postman 请求 curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=1JuO6V' "http://client:secret@localhost:8080/oauth/token"

    得到响应结果如下 { "access_token": "016d8d4a-dd6e-4493-b590-5f072923c413", "token_type": "bearer", "expires_in": 43199, "scope": "app" }

    携带令牌访问资源服务器

    此处以获取全部资源为例,其它请求方式一样,可以参考我源码中的单元测试代码。可以使用以下方式请求:

    使用 Headers 方式:需要在请求头增加 Authorization: Bearer yourAccessToken直接请求带参数方式:http://localhost:8081/contents?access_token=yourAccessToken

    使用 Headers 方式,通过 CURL 或是 Postman 请求

    curl --location --request GET "http://localhost:8081/contents" --header "Content-Type: application/json" --header "Authorization: Bearer yourAccessToken"

    Processed: 0.018, SQL: 9