1.9 整合Oauth2.0

1.9.1 Oauth2.0介绍

在实践之前我们先来了解下oauth2.0,OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。OAuth2.0服务提供者实际上分为:“授权服务 Authorization Service ”和“资源服务Resource Service”。Oauth2.0 的运行流程如下图所示:
运行流程

*(A)用户打开客户端,客户端要求用户给予授权。
*(B)用户同意给予客户端授权。
*(C)客户端使用上一步获得的授权(一般是Code),向认证服务器申请令牌TOKEN。
*(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
*(E)客户端使用令牌,向资源服务器申请获取资源(用户信息等)。
*(F)资源服务器确认令牌无误,同意向客户端开放资源。
图中服务端的三个组件分别为:

  • Resource Owner:即指需要授权访问的资源,比如用户昵称,头像
  • Authorization Server:鉴权服务,核心鉴权逻辑
  • Resource Server:资源服务
    Oauth2.0 定义了五种授权方式:
  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)
  • 扩展模式(Extension)

1.9.2 Oauth2.0授权模式

1.9.2.1 授权码模式(authorization_code)

授权码模式是功能最完整、流程最严密的授权模式。他的特点是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。以微信公众平台公众号网页应用开发流程为例。
授权码模式

步骤如下:

  • (A)用户访问客户端,客户端将用户导向认证服务器。
  • (B)用户选择是否给予客户端授权。
  • (C)若用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”(redirection URI),同时附上授权码code。
  • (D)客户端收到授权码code,附上早先的”重定向URI”,向认证服务器申请token。这一步是在客户端的后台的服务器上完成的,对用户不可见。
  • (E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

授权码模式的几个重要参数如下:

  • response_type:表示授权类型,必选项,此处的值固定为”code”

  • appid:表示客户端的ID,必选项

  • redirect_uri:表示重定向URI,可选项

  • scope:表示申请的权限范围,可选项

  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。用于防止恶意攻击

    授权码模式的URL以及参数应用步骤如下:

  • (1)引导用户跳转到授权页面:
    http://localhost:8080/oauth/authorize?client_id=client&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
    参数:
    • client_id 客户唯一标识
    • redirect_uri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
    • response_type 返回类型,请填写code
    • scope 应用授权作用域,有snsapi_base 、snsapi_userinfo 两种
    • state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
  • (2)通过code获取Token
    http://localhost:8080/oauth/token? client_id=client&secret=secret&code=CODE&grant_type=authorization_code
    参数:
    • client_id 客户唯一标识
    • secret 密钥
    • code 填写获取的code参数(存在有效期,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系)
    • grant_type 填写为authorization_code

返回结果:

1
2
3
4
5
6
7
{
"access_token":"ACCESS_TOKEN", //网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
"expires_in":7200, // access_token接口调用凭证超时时间,单位(秒)
"refresh_token":"REFRESH_TOKEN", //用户刷新access_token
"client_id":"client", //用户唯一标识
"scope":"all" //用户授权的作用域,使用逗号(,)分隔
}
  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

1.9.2.2 简化模式(implicit)

简化模式是不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过”授权码”步骤。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

简化模式

步骤如下:

  • (A)客户端将用户导向认证服务器。
  • (B)用户决定是否给于客户端授权。
  • (C)若用户授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
  • (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
  • (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
  • (F)浏览器执行上一步获得的脚本,提取出令牌。
  • (G)浏览器将令牌发给客户端。

下面是上面这些步骤所需要的参数。

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • response_type:表示授权类型,此处的值固定为”token”,必选项。
  • client_id:表示客户端的ID,必选项。
  • redirect_uri:表示重定向的URI,可选项。
  • scope:表示权限范围,可选项。
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值

例如:

1
2
3
4
5

GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz&redirect_uri=http://www.baidu.com
HTTP/1.1

Host: www.baidu.com

C步骤中,认证服务器回应客户端的URI,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
    例如:
    1
    2
    HTTP/1.1 302 Found
    Location: http://www.baidu.com#access_token=9e64da25-105c-4d20-a8b5-606c553c9b33&token_type=bearer&expires_in=829

认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。
根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。

1.9.2.3 密码模式(Password)

向客户端提供自己的用户名和密码,客户端使用这些信息,向”服务商提供商”索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

密码模式

步骤如下:

  • (A)用户向客户端提供用户名和密码。
  • (B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
  • (C)认证服务器确认无误后,向客户端提供访问令牌。
    在B步骤中,客户端发出的HTTP请求,包含以下参数:
  • grant_type:表示授权类型,此处的值固定为”password”,必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。

例如:

1
2
3
4
5
6

POST /oauth/token HTTP/1.1
Host: localhost
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=admin&password=111111

C步骤中,认证服务器向客户端发送访问令牌,例子:

1
2
3
4
5
6
7
8
9
10
11
12

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

整个过程中,客户端不得保存用户的密码。

1.9.2.4 客户端模式(client_credentials)

客户端模式下,并不存在对个体用户授权的行为,被授权的主体为client。因此,该模式可用于对某类用户进行集体授权。
客户端模式

申请该模式时,需要在HTTP request entity-body中提交以下信息。

1
2
3
4
5
6
7
8
POST /oauth/token
content-type: application/x-www-form-urlencoded
user-agent: PostmanRuntime/7.1.1
accept: */*
host: localhost:8080
accept-encoding: gzip, deflate
content-length: 74
grant_type=client_credentialsscope=allclient_id=zgqclient_secret=secret

若申请成功,服务器将返回access token和token有效时间。

1
2
3
4
5
6
7
8
9
10
11
12

HTTP/1.1 200
status: 200
cache-control: no-store
pragma: no-cache
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: DENY
content-type: application/json;charset=UTF-8
transfer-encoding: chunked
date: Thu, 20 Jun 2019 09:01:58 GMT
{"access_token":"43f856cb-2976-4a73-85f3-75638c32f8d7","token_type":"bearer","expires_in":1199,"scope":"all"}

1.9.2.5 扩展模式

扩展模式也叫自定义模式。Oauth2.0的规范中要求 “grant type”参数必须为URI。对于其他申请数据,可以根据需求进行自定义。这里不对这个部分做深入讨论,如果需要可以进一步查看Oauth2.0 相关文档。

1.9.2.6 令牌更新

在用户访问的时候,客户端的”访问令牌”如果已经过期,则需要使用”更新令牌”申请一个新的访问令牌。客户端可以发出更新令牌的HTTP请求进行令牌更新。
令牌更新请求包含以下参数:

  • grant_type:表示使用的授权模式,此处的值固定为”refresh_token”,必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
  • client_id: 客户唯一标识
  • client_secret:密钥

例如:
请求内容:

1
2
POST /oauth/token
refresh_token=67ced428-1011-4da5-ae54-f3b98cb46b01&grant_type=refresh_token&scope=all&client_id=zgq&token_type=bearer&client_secret=secret

返回内容:

1
2
HTTP/1.1 200
{"access_token":"ab827d16-0a6b-4cdd-abaa-ad6535de9881","token_type":"bearer","refresh_token":"67ced428-1011-4da5-ae54-f3b98cb46b01","expires_in":11999,"scope":"all"}

1.9.3 SpringBoot整合Oauth2.0 和Spring Security

1.9.3.1 Step1:创建Oauth2.0所需要的三个表

Mysql脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
`refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

1.9.3.2 Step2:添加POM配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<!--安全验证相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.14.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

1.9.3.3 Step3:配置数据源

在采用数据库存储的Oauth认证信息时需要使用数据源访问数据库。需要在application-dev.properties 文件中增加以下配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#DataSource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/splus?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = root
初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20

同时还需要使用注解配置DataSource组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.sql.SQLException;

/**
* DruidDataSourceConfig
* Druid数据源管理配置类
*
* @author: zone7
* @time: 2018.08.20
*/
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidDataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName;
private int initialSize;
private int minIdle;
private int maxActive;
private int maxWait;
private int timeBetweenEvictionRunsMillis;
private int minEvictableIdleTimeMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean poolPreparedStatements;
private int maxPoolPreparedStatementPerConnectionSize;
private String filters;
private String connectionProperties;

// 解决 spring.datasource.filters=stat,wall,log4j 无法正常注册
@Bean
@Primary // 在同样的DataSource中,首先使用被标注的DataSource
public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(url);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);

// configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {
System.err.println("druid configuration initialization filter: " + e);
}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getDriverClassName() {
return driverClassName;
}

public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}

public int getInitialSize() {
return initialSize;
}

public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}

public int getMinIdle() {
return minIdle;
}

public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}

public int getMaxActive() {
return maxActive;
}

public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}

public int getMaxWait() {
return maxWait;
}

public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}

public int getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}

public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}

public int getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}

public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}

public String getValidationQuery() {
return validationQuery;
}

public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}

public boolean isTestWhileIdle() {
return testWhileIdle;
}

public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}

public boolean isTestOnBorrow() {
return testOnBorrow;
}

public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}

public boolean isTestOnReturn() {
return testOnReturn;
}

public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}

public boolean isPoolPreparedStatements() {
return poolPreparedStatements;
}

public void setPoolPreparedStatements(boolean poolPreparedStatements) {
this.poolPreparedStatements = poolPreparedStatements;
}

public int getMaxPoolPreparedStatementPerConnectionSize() {
return maxPoolPreparedStatementPerConnectionSize;
}

public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}

public String getFilters() {
return filters;
}

public void setFilters(String filters) {
this.filters = filters;
}

public String getConnectionProperties() {
return connectionProperties;
}

public void setConnectionProperties(String connectionProperties) {
this.connectionProperties = connectionProperties;
}
}

1.9.3.4 Step4:新建Spring安全用户明细

用户明细类必须继承org.springframework.security.core.userdetails.User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import com.zone7.admin.sys.pojo.SysUser;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class MyUserDetails extends User{
private static List grants = Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
private UserDto user;

public MyUserDetails(UserDto user) {

super(user.getUsername(), user.getPassword(), true, true, true, true, grants);
this.user = user;
}

public UserDto getUser() {
return user;
}

public void setUser(UserDto user) {
this.user = user;
}
}

1.9.3.5 Step5:新建用户验证服务

用于加载用户信息,当Oauth2.0的 authorization_code或者password授权模式的时候需要使用这个类验证用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

import com.zone7.admin.sys.dto.MyUserDetails;
import com.zone7.admin.sys.dto.UserDto;
import com.zone7.admin.sys.pojo.SysUser;
import com.zone7.admin.sys.service.SysUserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
* @ClassName: UserDetailsServiceImpl
* @Description: TODO
* @Author: zgq
* @Date: 2019/6/19 21:16
* @Version: 1.0
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private SysUserService sysUserService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByKeyword(username);
if(user==null){
throw new UsernameNotFoundException("用户不存在!");
}else{
return UserDetailConverter.convert(user);
}
}

private static class UserDetailConverter {
static UserDetails convert(SysUser user) {
UserDto dto=new UserDto();
BeanUtils.copyProperties(user,dto);
return new MyUserDetails(dto);
}
}
}

1.9.3.6 Step6:新增密钥编码解码器

主要用于密码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

import com.zone7.admin.utils.SHA256Util;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* 密钥编码器
* @Authod zone7
*/
public class Sha256PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return SHA256Util.getSHA256StrJava(charSequence.toString());
}

@Override
public boolean matches(CharSequence charSequence, String s) {

return s.equals(SHA256Util.getSHA256StrJava(charSequence.toString()));
}
}

1.9.3.7 Step7:新增配置类

  • (1) Oauth2.0授权配置类AuthorizationServerConfigurerAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

package com.zone7.admin.config.oauth2;

import com.zone7.admin.sys.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configurers.ldap.LdapAuthenticationProviderConfigurer;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;

/**
* 授权服务配置
*/
@Configuration
@EnableAuthorizationServer
@Order(7)
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {

@Bean // 声明TokenStore实现
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 注入authenticationManager
* 来支持 password grant type
*/
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetails;
@Autowired
private UserDetailsServiceImpl userDetailsService;

@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}


@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.passwordEncoder(new MyPasswordEncoder())
.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
.allowFormAuthenticationForClients();
}




@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(tokenStore());
endpoints.userDetailsService(userDetailsService);
endpoints.setClientDetailsService(clientDetails);

//配置TokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());

tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 1天
endpoints.tokenServices(tokenServices);
}



@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(new MyPasswordEncoder());


}

}
  • (2) 资源配置类ResourceServerConfigurerAdapter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

    /**
    * 资源服务配置
    */
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
    http.requestMatchers().antMatchers("/api/**")
    .and()
    .authorizeRequests().antMatchers("/api/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId("resourcesId").stateless(true);
    }

    }
  • (3) WEB安全配置类WebSecurityConfigurerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

import com.zone7.admin.sys.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@Order(100)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.anonymous().disable();

http.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**","/login/**","/logout/**").authenticated()
.and()

.formLogin().permitAll();


}

@Override
public void configure(WebSecurity web) throws Exception {
//设置静态资源不要拦截
web.ignoring().antMatchers("/js/**","/cs/**","/images/**");
}
@Autowired
private UserDetailsServiceImpl userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new Sha256PasswordEncoder());

// auth.inMemoryAuthentication().passwordEncoder(new Sha256PasswordEncoder())
// .withUser("admin").password("123456").roles("ADMIN","USER").and()
// .withUser("user").password("111").roles("USER");
}

/**
* 需要配置这个支持password模式 support password grant type
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

}

1.9.3.8 Step8:测试

启动数据和应用程序,使用Postman
首先添加一个http请求,请求地址为资源配置类中配置的路径,例如:
postman测试oauth2

点击Get New Access Token 按钮。分别使用不同的授权模式测试授权服务器获取令牌的功能。

postman测试oauth2 token1
postman测试oauth2 token2
postman测试oauth2 token3

点击Request Token 得到Token
postman测试oauth2 get token