创建资源服务器
概述
在 为什么需要 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>
<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>
<dependency>
<groupId>org.springframework.cloud
</groupId>
<artifactId>spring-cloud-starter-oauth2
</artifactId>
</dependency>
<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>
<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()
.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
;
@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"