# OAuth2-----授权码模式使用
# 一、定义
1、定义:
OAuth 2.0是一种授权框架,用于授权第三方应用访问用户的资源,如照片、个人信息等,OAuth 2.0具有高度的安全性和可扩展性,被广泛应用于各种开放平台的接口鉴权,是目前应用最广泛的开放平台鉴权方式之一
2、使用场景:
例如,一个终端用户(资源所有者)可以授权一个打印服务(客户端)访问她存储在照片共享服务(资源服务器)上的受保护照片,而无需与打印服务共享她的用户名和密码。相反,她直接与受照片共享服务信任的服务器(授权服务器)进行身份验证,该服务器颁发打印服务委托特定的凭据(访问令牌)。
第三方应用授权登录:在APP或者网页接入一些第三方应用时,时常会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录,第三方应用通过oauth2方式获取用户信息
# 二、流程图
![流程图](images/1-377.png)
参考官网定义:https://datatracker.ietf.org/doc/html/rfc6749
名词解释:
resource owner:资源所有者
resource server:资源服务器
Client:客户
authorization server:授权服务器
### 流程说明:
(A)客户端向资源所有者请求授权。
(B) 客户端收到授权授予,该授权授予是表示资源所有者的授权的凭证。
(C)客户端携带授权凭证,向授权服务器申请令牌。
(D)授权服务器对客户端进行身份验证并验证授权授予,如果有效,则颁发访问令牌。
(E)客户端使用令牌,向资源服务器申请资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
### 相应官网:
https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-resource-server-access-token-jwt --- spring-security官网
https://docs.spring.io/spring-security-oauth2-boot/docs/ --- spring-security-oauth2-boot各个版本
https://docs.spring.io/spring-security-oauth2-boot/docs/2.7.x/reference/html5/ --- spring-security-oauth2-boot对应版本官网
https://spring.io/projects/spring-cloud-security#learn -- Spring Cloud Security官网
https://www.springcloud.cc/spring-cloud-greenwich.html#_spring_cloud_security --- 第十一部分。Spring Cloud Security
### 其他网页参考:
http://websystique.com/spring-security/secure-spring-rest-api-using-oauth2/ --- Secure Spring REST API using OAuth2
https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html --- 理解OAuth 2.0
https://www.ruanyifeng.com/blog/2019/04/oauth_design.html --- OAuth 2.0 的一个简单解释
https://codeleading.com/article/73121122186/ ----Spring Security 源码分析(四):OAuth2 实现
# 三、分类
### OAuth 2.0定义了四种授权方式。
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
### 1、授权码模式:
![授权码模式流程图](images/1-2497.png)
(A)客户端通过引导用户代理到授权端点。
(B)授权服务器对用户认证,并让用户确认是否选择授权
(C)如果用户确认授权访问权限,授权服务器将根据重定向URI返回一个授权码,包括其他参数。
(D)客户端收到授权码,附上刚才的重定向URI,向授权服务器申请令牌。由客户端的后台向授权服务器申请完成。
(E)授权服务器确认了 授权码和重定向URI的信息没错,然后向客户端发送访问令牌(access token)和更新令牌(refresh token)。
##### 1.1、请求授权码:
~~~
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
~~~
#### 1.1.1.请求参数:
response_type:表示授权类型,必填,此处的值固定为"code"
client_id:表示客户端的ID,且唯一,必填
redirect_uri:表示重定向URI,可填,最好带上,并且是外网可访问
scope:表示申请的权限范围,可填 "all"
state:任意值,原样返回。
#### 1.1.2.响应结果:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
其中code值即为返回的授权码,需要保留,接下来,请求令牌
#### 1.2、请求令牌:
~~~
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
~~~
#### 1.2.1.请求参数:
grant_type:表示授权模式,必填项,此处的值固定为"authorization_code"。
code:表示第一步所获得的授权码,必填项。
redirect_uri:重定向URI,必填项,且必须与第一步中的URI一样。
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
重点为"Authorization",其中的值"czZCaGRSa3F0MzpnWDFmQmF0M2JW"为client_id:client_secret,客户端ID与客户端密码组合,使用英文的冒号:连接的base64格式编码格式,Basic 为固定值
Content-Type为请求类型,一定是”application/x-www-form-urlencoded”
##### 1.2.2.响应结果:
~~~
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"
}
~~~
access_token:表示访问令牌。
token_type:表示令牌类型,该值大小写不敏感,可以是bearer类型或mac类型。
expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token:表示更新令牌,用来获取下一次的访问令牌,设置authorizedGrantTypes时带有"refresh_token"才会返回该值
scope:表示权限范围,一般省略。
### 2、简化模式:
![简化模式流程图](images/1-4377.png)
(A)客户端通过引导用户代理到授权端点。
(B)授权服务器对用户认证,并让用户确认是否选择授权。
(C)如果用户同意授权,授权服务器将根据URI重定向回来,并带着令牌。
(D)用户向资源服务器请求,其中不包括上一步返回的信息。
(E)资源服务器返回一个网页,其中包含有C步请求中返回的信息。
(F)浏览器执行资源服务器返回的脚本,提取访问令牌。
(G)浏览器将访问令牌传递给客户端。
##### 2.1、请求认证
~~~
GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
~~~
请求类型还是application/x-www-form-urlencoded
##### 2.1.1.请求参数
response_type:表示授权类型,此处的值固定为"token",必填项。
client_id:表示客户端的ID,必填项。
redirect_uri:表示重定向的URI,可填项。
scope:表示权限范围,可填项。
state:任意值,原样返回。
##### 2.2.请求资源:
~~~
HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
&state=xyz&token_type=example&expires_in=3600
~~~
请求类型还是application/x-www-form-urlencoded
##### 2.2.1.请求参数:
access_token:表示访问令牌,必填项。
token_type:表示令牌类型,该值大小写不敏感,必填项。
expires_in:表示过期时间,单位为秒。
scope:表示权限范围,可省略。
state:任意值,原样返回。
### 3、密码模式
![密码模式流程图](images/1-5254.png)
(A) 资源所有者向客户端提供其用户名和密码
(B) 客户端将用户信息向授权服务器请求,授权服务器进行身份验证。
(C) 授权服务器对客户端进行身份验证并验证
资源所有者凭据,如果有效,则提供访问令牌
#### 3.1、请求令牌
```
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
```
##### 3.1.1.请求参数:
grant_type:表示授权类型,此处的值固定为"password",必填项。
username:表示用户名,必填项。
password:表示用户的密码,必填项。
scope:表示权限范围,可填项。
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
重点为"Authorization",其中的值"czZCaGRSa3F0MzpnWDFmQmF0M2JW"为client_id:client_secret,客户端ID与客户端密码组合,使用英文的冒号:连接的base64格式编码格式,Basic 为固定值
Content-Type为请求类型,一定是”application/x-www-form-urlencoded”
##### 3.1.2.响应结果:
```
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"
}
```
## 4、客户端模式
![客户端模式流程图](images/1-6261.png)
(A) 客户端通过授权服务器进行身份验证,并且请求访问令牌。
(B) 授权服务器对客户端进行认证,并提供访问令牌。
#### 4.1请求令牌
```
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
```
##### 4.1.1请求参数:
granttype:表示授权类型,此处的值固定为"client_credentials",必填项。
scope:表示权限范围,可填项。
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
重点为"Authorization",其中的值"czZCaGRSa3F0MzpnWDFmQmF0M2JW"为client_id:client_secret,客户端ID与客户端密码组合,使用英文的冒号:连接的base64格式编码格式,Basic 为固定值
Content-Type为请求类型,一定是”application/x-www-form-urlencoded”
##### 4.1.2响应结果:
```
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,
"example_parameter":"example_value"
}
```
## 5、刷新令牌
```
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
```
### 5.1请求参数:
grant_type:表示使用的授权模式,此处的值固定为"refresh_token",必填项。
refresh_token:表示早前收到的更新令牌,必填项。
scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
重点为"Authorization",其中的值"czZCaGRSa3F0MzpnWDFmQmF0M2JW"为client_id:client_secret,客户端ID与客户端密码组合,使用英文的冒号:连接的base64格式编码格式,Basic 为固定值
Content-Type为请求类型,一定是”application/x-www-form-urlencoded”
### 5.2响应结果
```
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)Third-party application:第三方应用程序
(2)HTTP service:HTTP服务提供商。
(3)Resource Owner:资源所有者。
(4)User Agent:用户代理,就是指浏览器。
(5)Authorization server:授权服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与授权服务器,可以是同一台服务器,也可以是不同的服务器。
(7)client:受保护资源请求的应用程序
(8)client_id:客户端标识
(9)client_secret:客户端秘钥
# 五、开始引入jar
本例使用
```
SpringBoot 2.1.14.RELEASE版本,
Spring cloud Greenwich.SR5版本,
Spring cloud alibaba 2.1.2.RELEASE版本,
JDK8,
Mysql 5.7,
Maven3.8,
Spring-security-config 5.1.10
Spring-security-oauth2 2.3.4.RELEASE
```
主要对授权码模式进行简单配置,在pom.xml中引入
```
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
对于微服务项目,引入这个spring-cloud-starter-oauth2即可,里面已经包含的oauth2和security的jar;如果是spring boot 单体项目,可引入spring-security-oauth2-autoconfigure或者其他,其他功能所需依赖jar自行按需引入。
注意:
Spring Security OAuth 已经停止更新
<https://spring.io/projects/spring-security-oauth>
![过期图](images/1-7234.png)
### SQL脚本:
~~~
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` longblob NULL,
`authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` longblob NULL,
`refresh_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`clientId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`expiresAt` datetime(0) NULL DEFAULT NULL,
`lastModifiedAt` datetime(0) NULL DEFAULT NULL
) 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(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(255) 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(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('clients', 'resource-api', '$2a$10$bCaomfHma4JaQmK1HOVk2.AkXr71jLhOQCORZQnqIgClYv.HncMry', 'all', 'authorization_code,password,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, 'false');
-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` longblob NULL,
`authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` longblob NULL DEFAULT NULL
) 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(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` longblob NULL,
`authentication` longblob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
`permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单地址',
`parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员角色');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`, `PID`) USING BTREE,
INDEX `FK_Reference_12`(`PID`) USING BTREE,
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
`password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`status` int(1) NULL DEFAULT 1 COMMENT '1开启0关闭',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '123456', 1);
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`, `RID`) USING BTREE,
INDEX `FK_Reference_10`(`RID`) USING BTREE,
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
SET FOREIGN_KEY_CHECKS = 1;
~~~
# 六、授权服务器
认证(Authentication):用来验证用户是否具有访问系统的权限
授权(Authorization):用来验证用户是否具有访问某个资源的权限,如果授权通过,该用户就能对资源做增删改查等操作
@Configuration
@EnableAuthorizationServer
注解并继承AuthorizationServerConfifigurerAdapter来配置OAuth2.0 授权服务器。
AuthorizationServerConfifigurerAdapter要求配置
ClientDetailsServiceConfifigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化
AuthorizationServerEndpointsConfifigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)。
AuthorizationServerSecurityConfifigurer:用来配置令牌访问端点的安全约束
## 代码如下:
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.tokenServices(tokenServices) // 令牌管理
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 授权服务的名字
.withClient("clients-auth")
// 授权密码
.secret(passwordEncoder.encode("auth3366"))
// 重定向位置
.redirectUris("http://www.baidu.com")
// 授权范围
.scopes("all")
// 是否自动批准
.autoApprove(false)
// 授权类型
.authorizedGrantTypes("authorization_code","refresh_token");
}
// 配置授权码储存,暂时存在内存中,可存于内存与数据库中,使用jwt 注释掉
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
// 资源服务器到授权服务器验证AccessToken ,两种方式,资源服务器每接收一次请求,都到授权服务器认证AccessToken
// 第二种是:到数据请求数据认证,前提是授权服务器已经把AccessToken存入数据库中,
// 其中 第一种每次都去授权服务器认证的,又分两种 RemoteTokenServices 和 DefaultTokenServices(默认) 。而DefaultTokenServices 不适合认证资源服务分离部署
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情,从容器中获取
tokenServices.setTokenStore(tokenStore);//token的存储方式
// 是否支持 refreshToken
tokenServices.setSupportRefreshToken(true);
// 是否复用 refreshToken
tokenServices.setReuseRefreshToken(false);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24);
//默认30天,这里修改
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
/**
* 内存存储采用的是TokenStore接口默认实现类InMemoryTokenStore,开发时方便调试,适用单机版
* InMemoryTokenStore:token存储内存之中(默认,不适合认证资源服务分离部署)
* JdbcTokenStore:token存储在关系型数据库之中
* JwtTokenStore:token不会存储到任何介质中,使用JWT令牌作为AccessToken,在请求发起者和服务提供者之间网络传输
* RedisTokenStore:token存储在Redis数据库之中
* @return
*/
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
return new InMemoryApprovalStore();
}
}
~~~
userDetailsService:可从数据库获取账号信息,在这里简单封装一下
```java
@Slf4j
@Component
public class AuthUserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String encode = passwordEncoder.encode("123456");
/**
* 获取用户信息得逻辑,自定义权限规则
**/
return new User(username,encode, AuthorityUtils.createAuthorityList("admin"));
}
}
```
# 七、资源服务器
application.xml配置oauth2的参数:
~~~yaml
security:
oauth2:
client:
clientId: clients-auth
clientSecret: auth3366
userAuthorizationUri: http://localhost:8002/oauth/authorize
accessTokenUri: http://localhost:8002/oauth/token
registeredRedirectUri: http://localhost:8002/oauth/authorize
resource:
id: clients
userInfoUri: http://127.0.0.1:8003/api/user/getCurrentUser
tokenInfoUri: http://localhost:8002/oauth/check_token
authorization:
checkTokenAccess: http://localhost:8002/oauth/check_token
~~~
### 代码如下:
~~~java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.client.clientId}")
private String clientId;
@Value("${security.oauth2.client.clientSecret}")
private String secret;
@Value("${security.oauth2.authorization.checkTokenAccess}")
private String checkTokenEndpointUrl;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint; // 自定义错误处理,implements AuthenticationEntryPoint ,不添加也可以
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authExceptionEntryPoint).tokenServices(tokenService()).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 对相关请求进行授权
.anyRequest()// 任意请求
.authenticated()// 都要经过验证
/*.and()
.requestMatchers()// 设置要保护的资源
.antMatchers("/**")*/;// 保护的资源
}
//默认是DefaultTokenServices,但是使用 RemoteTokenServices 远程检验token,因为时使用内存演示,授权服务与资源服务是分开的,启动服务的时候开辟的内存不一致,所以不能使用DefaultTokenServices的token认证模式
public RemoteTokenServices tokenService() {
// 每次请求都要去授权服务 认证,增加了网络开销
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setClientId(clientId);
tokenService.setClientSecret(secret);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
return tokenService;
}
}
~~~
# 八、Security
在授权项目中添加security配置,注意本版。在spring security 5.2版本以后该写法已经摒弃,具体请查看官网示例
~~~java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Lazy
@Autowired
private AuthUserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**","/login/**","/logout/**")
.permitAll()
.anyRequest()
.authenticated()
// .and()
// .formLogin() // FormLogin模式
.and()
.httpBasic() // 开启HttpBasic模式
.csrf()
.disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
~~~
如果不配置登入页面。Spring security 会构造一个登入页,而这个任务就交给了DefaultLoginPageGeneratingFilter这个过滤器
默认退出的过滤器为DefaultLogoutPageGeneratingFilter
登入认证大概经过的过滤器
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
Spring Security 有:
HttpBasic模式登录认证、
formLogin模式登录认证
两种模式,Basic模式比较简单也不那么安全,不建议在生产环境使用,但是可用来演示。在security的5.x版本之前都是默认Basic的模式,在5.x版本之后默认使用form模式,form模式可自定义登入页面属性路径,具体操作请自行上网查看相关信息
# 九、内存方式请求
~~~java
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 授权服务的名字
.withClient("clients-auth")
// 授权密码
.secret(passwordEncoder.encode("auth3366"))
// 重定向位置
.redirectUris("http://www.baidu.com")
// 授权范围
.scopes("all")
// 是否自动批准
.autoApprove(false)
// 授权类型
.authorizedGrantTypes("authorization_code","refresh_token");
}
/**
* 内存存储采用的是TokenStore接口默认实现类InMemoryTokenStore,开发时方便调试,适用单机版
* @return
*/
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
return new InMemoryApprovalStore();
}
~~~
# 十、授权码
请求授权码:
http://127.0.0.1:8002/oauth/authorize?client_id=clients-auth&response_type=code&scope=all&redirect_uri=http://www.baidu.com
1、登录页 账号密码是数据库的中系统用户的账号密码:admin 123456,授权选择按钮 Approve
|字段|描述|
|:----:|:----|
|response_type | 必填。将其值设置为code表示如果成功的话将收到一个授权码。|
|client_id | 必填。客户端标识。|
|redirect_uri | 可选。重定向URI虽然不是必须的,但是你的服务应该会需要它。而且,这个URL必须和授权服务端注册的redirect_id一致。|
|scope | 可选。请求可能会有一个或多个scope值。授权服务器会把客户端请求的范围(scope)展示给用户看。|
|state | 推荐。state参数用于应用存储特定的请求数据的可以防止CSRF攻击。授权服务器必须原封不动地将这个值返回给应用。|
![登入图](images/1-26867.png)
![授权图](images/1-26870.png)
选择“Approve ”授权
重定向到百度页面,并带着授权码
![](images/1-26903.png)
# 十一、获取token
获取code=AIljsQ,再postman工具中请求access_token,请求方式请参考官网说明:
<https://datatracker.ietf.org/doc/html/rfc6749#autoid-36>
请求"/oauth/token" , post请求
|字段|描述|
|:-------:|:-----|
|response_type | 必填。授权码模式固定值authorization_code。|
|client_id | 非必填。主要与请求方式有关,使用头部Authorization: Basic xxxx方式,就不用填写,直接把client_id:client_secret 使用base64加密方式加到Basic后面,官方提示必须填,未经身份验证的客户端必须发送其“client_id”,如果不是使用Basic方式,则必填 |
|client_secret | 非必填。主要与请求方式有关,使用头部Authorization: Basic xxxx方式,就不用填写,直接把client_id:client_secret 使用base64加密方式加到Basic后面,如果不是使用Basic方式,则必填 |
|redirect_uri | 必填 |
|code | 必填,即上一步请求返回的授权码 |
加密client-id:client-secret形式的客户端id与私钥,用英文分号隔开,base64加密方式,加在header中,
参考 https://datatracker.ietf.org/doc/html/rfc2617#autoid-4
如:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
~~~java
public static void main(String[] args) {
System.out.println(new org.apache.tomcat.util.codec.binary.Base64().encodeAsString("clients-1:12345678".getBytes()));
System.out.println(java.util.Base64.getEncoder().encodeToString("clients-1:12345678".getBytes()));
}
~~~
![](images/1-28028.png)
![](images/1-28030.png)
TokenEndpoint.class的postAccessToken为生成token的方法,但在进入该方法之前,
先判断认证是走client认证还是Authorization认证。首先
在AbstractAuthenticationProcessingFilter.doFilter
![](images/1-28174.png)
在ClientCredentialsTokenEndpointFilter.ClientCredentialsRequestMatcher.matches中判断请求参数中存在client_id,则走client客户端认证方式
![](images/1-28289.png)
如果不存在,则进入到BasicAuthenticationFilter.doFilterInternal解释出Authorization的参数,
不管哪种方式最后都会在BasicAuthenticationFilter.doFilterInternal中进入到TokenEndpoint的postAccessToken生成token
# 十二、拿token获取数据
localhost:8003/api/user/getCurrentUser 加上一步返回的token
请求资源验证入口为OAuth2AuthenticationProcessingFilter.class,
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {}
![](images/1-28666.png)
进入提取token,Authentication authentication = tokenExtractor.extract(request);在BearerTokenExtractor.extractv.extractToken.extractHeaderToken中提取token
![](images/1-28813.png)
也可以通过access_token参数带着token请求,然后封装成authentication一个对象
![](images/1-28868.png)
在进入OAuth2AuthenticationManager认证管理,封装RemoteTokenServices的token验证对象,这一步很关键
![](images/1-28944.png)
封装请求头
![](images/1-28952.png)
远程请求验证token
![](images/1-28966.png)
随即进入验证身份的入口:AbstractAuthenticationProcessingFilter,重复验证的步骤,还是走的Authorization认证方式
![](images/1-29049.png)
![](images/1-29051.png)
重新把请求对封装到上下文中
![](images/1-29067.png)
进入到授权服务的token管理服务
![](images/1-29087.png)
进入到token的校验方法中CheckTokenEndpoint
![](images/1-29122.png)
最后验证成功后返回到远程管理token方法中来
![](images/1-29148.png)
回到认证管理OAuth2AuthenticationManager
![](images/1-29184.png)
回到验证入口,把对象放入到上写文中OAuth2AuthenticationProcessingFilter
![](images/1-29240.png)
最后到调用方法
![](images/1-29250.png)
返回结果
![](images/1-29258.png)
# 十三、JDBC方式
### 1、数据库添加一条数据
~~~sql
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('clients-auth', 'admin_resourceids', '$2a$10$9jBvNKTxv3A1Tv5fgfpHd.9kgm9f2Diei.9voOYazRo/tntrCcHWO', 'all', 'authorization_code,refresh_token,password', 'http://www.baidu.com', NULL, 3600, 36000, NULL, '1');
~~~
### 2、修改授权服务器:
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
@Resource
private DataSource dataSource;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.tokenServices(tokenServices) // 令牌管理
.tokenStore(tokenStore) // token存储数据库
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库中获取客户端信息
clients.withClientDetails(clientDetails());
}
/**
* 从数据库中获取客户端详情
* @return
*/
public ClientDetailsService clientDetails() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
// 配置授权码储存,暂时存在内存中,可存于内存与数据库中
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
// 授权码存于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
// 配置管理令牌服务 ,DefaultTokenServices 和 RemoteTokenServices
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情,从容器中获取
tokenServices.setTokenStore(tokenStore);//基于数据库存储令牌,管理token的方式
// 是否支持 refreshToken
tokenServices.setSupportRefreshToken(true);
// 是否复用 refreshToken
tokenServices.setReuseRefreshToken(false);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24);
//默认30天,这里修改
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
/**
* 内存存储采用的是TokenStore接口默认实现类InMemoryTokenStore,开发时方便调试,适用单机版
* @return
*/
@Bean
public TokenStore tokenStore(){
// token存储于数据库
return new JdbcTokenStore(dataSource);
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
// 授权码存储数据库
return new JdbcApprovalStore(dataSource);
}
}
~~~
### 3、修改资源服务器:
~~~java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.id}")
private String resourceId;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Resource
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authExceptionEntryPoint)
.tokenServices(tokenService())
.resourceId(resourceId) // 如果要配置这个resourceId,那这里的resourceId必须要跟授权服务中数据库字段resourceId的值一致
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 对相关请求进行授权
.anyRequest()// 任意请求
.authenticated()// 都要经过验证
/*.and()
.requestMatchers()// 设置要保护的资源
.antMatchers("/**")*/;// 保护的资源
}
/**
* 改用存缓存校验token,token的认证方式
* @return
*/
public DefaultTokenServices tokenService(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}
@Bean
public TokenStore tokenStore() {
//使用数据库
return new JdbcTokenStore(dataSource);
}
}
~~~
### 4、请求:
授权码:
![](images/1-34896.png)
获取token:
![](images/1-34907.png)
先到数据库查询是否存在token
![](images/1-34926.png)
不存在时,则插入token到数据库
![](images/1-34946.png)
![](images/1-34948.png)
请求资源,到数据库查询token
![](images/1-34968.png)
![](images/1-34970.png)
![](images/1-34973.png)
# 十四、Redis方式
### 1、配置redis对象
~~~java
@Configuration
public class RedisTokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore (){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
// redis:oauth2使用的是java二进制序列化的方式,存在一些问题,最好能重写RedisTokenStore序列化方式
//1. 如果UserDetails定义的字段发生增删,已存在的token,访问校验的时候,就会发生序列化错误;
//2. 如果去redis中查看某个token的内容的时候,会发现全是乱码,完全看不懂;
redisTokenStore.setSerializationStrategy(new JdkSerializationStrategy());
return redisTokenStore;
}
}
~~~
### 2、修改授权服务
只修改token的管理方式,其他信息,如客户端,授权码等仍存于数据库
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
@Resource
private DataSource dataSource;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.tokenServices(tokenServices) // 令牌管理
.tokenStore(tokenStore) // token存储redis
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库中获取客户端信息
clients.withClientDetails(clientDetails());
}
/**
* 从数据库中获取客户端详情
* @return
*/
public ClientDetailsService clientDetails() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
// 配置授权码储存,可存于内存与数据库中
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
// 授权码存于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
// 配置管理令牌服务 ,DefaultTokenServices 和 RemoteTokenServices
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情,从容器中获取
tokenServices.setTokenStore(tokenStore);//基于redis存储令牌,管理token的方式
// 是否支持 refreshToken
tokenServices.setSupportRefreshToken(true);
// 是否复用 refreshToken
tokenServices.setReuseRefreshToken(false);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24);
//默认30天,这里修改
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
// 授权码存储数据库
return new JdbcApprovalStore(dataSource);
}
}
~~~
### 3、修改资源服务器
~~~java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.id}")
private String resourceId;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Resource
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authExceptionEntryPoint)
.tokenServices(tokenService())
.resourceId(resourceId) // 如果要配置这个resourceId,那这里的resourceId必须要跟授权服务中数据库字段resourceId的值一致
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 对相关请求进行授权
.anyRequest()// 任意请求
.authenticated()// 都要经过验证
}
/**
* 改用存缓存校验token,token的认证方式
* @return
*/
public DefaultTokenServices tokenService(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}
}
~~~
### 4、请求
![](images/1-40294.png)
![](images/1-40296.png)
# 十五、Jwt是什么
JSON Web令牌(JWT)(全称:Json Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于作为JSON对象在各方之间安全地传输信息。此信息是经过数字签名的,因此可以验证和信任
官方:<https://jwt.io/introduction/>
<https://jwt.io/#debugger-io> ---加解密
![](images/1-40575.png)
# 十六、Jwt包含哪些
### 1、JSON Web Token
由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
```
Header(头部)
Payload(负载)
Signature(签名)
```
一个典型的JWT看起来是这个样子的
```
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
```
### 2、Header
由两部分组成,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
```
{
'alg': "HS256",
'typ': "JWT"
}
```
### 3、Payload
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
Public claims : 可以随意定义。
Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用
* iss (issuer):签发人
* exp (expiration time):过期时间
* sub (subject):主题
* aud (audience):受众
* nbf (Not Before):生效时间
* iat (Issued At):签发时间
* jti (JWT ID):编号
### 4、Signature
是对前两部分的签名,防止数据篡改,需要一个密钥,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),对header部分、payload部分进行签名
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户
### 5、JWT 的使用方式
服务器返回给客户端JWT后,可以放在cookie或者缓存localStorage中,但最好是放在 HTTP 请求的头信息Authorization字段里面,这样更方便跨域请求
# 十七、JWT方式
为什么使用JWT:令牌采用JWT格式,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,只需要发送请求时带上JWT(一般放在请求头)访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权
## 1、普通方式(对称加密):
### 1.1、修改授权服务
#### 1.1.1、添加JwtTokenStore
```java
@Configuration
public class JwtTokenConfig {
private static final String JWT_SIGN="admin-sign";
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey(JWT_SIGN);
return accessTokenConverter;
}
}
```
#### 1.1.2、修改授权服务
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
@Resource
private DataSource dataSource;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.tokenServices(tokenServices) // 令牌管理
.tokenStore(jwtTokenStore) // 基于jwt管理token
.accessTokenConverter(jwtAccessTokenConverter)
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库中获取客户端信息
clients.withClientDetails(clientDetails());
}
/**
* 从数据库中获取客户端详情
* @return
*/
public ClientDetailsService clientDetails() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
// 配置授权码储存,可存于内存与数据库中
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
// 授权码存于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
// 配置管理令牌服务 ,DefaultTokenServices 和 RemoteTokenServices
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情,从容器中获取
tokenServices.setTokenStore(jwtTokenStore);//基于redis存储令牌,管理token的方式
// 是否支持 refreshToken
tokenServices.setSupportRefreshToken(true);
// 是否复用 refreshToken
tokenServices.setReuseRefreshToken(false);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24);
//默认30天,这里修改
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
// 授权码存储数据库
return new JdbcApprovalStore(dataSource);
}
}
~~~
### 1.2、修改资源服务
#### 1.2.1、配置:
```yaml
security:
oauth2:
resource:
id: admin_resourceids #// 必须与授权服务的resourceIds一致,不然就干脆都不配置,ResourceServerSecurityConfigurer默认一个
```
#### 1.2.2、添加jwt:
~~~java
@Configuration
public class JwtTokenConfig {
private static final String JWT_SIGN="admin-sign";
@Bean
public TokenStore jwtTokenStore() throws Exception {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() throws Exception {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey(JWT_SIGN);
return accessTokenConverter;
}
}
~~~
#### 1.2.3、修改ResourceServerConfig
~~~java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.id}")
private String resourceId;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Autowired
private TokenStore jwtTokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authExceptionEntryPoint) // 认证失败时候调用
.resourceId(resourceId) // 必须与授权服务的resourceIds一致,不然就干脆都不配置,ResourceServerSecurityConfigurer默认一个
.tokenStore(jwtTokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 对相关请求进行授权
.anyRequest()// 任意请求
.authenticated()// 都要经过验证
/*.and()
.requestMatchers()// 设置要保护的资源
.antMatchers("/**")*/;// 保护的资源
}
}
~~~
![](images/1-47652.png)
![](images/1-47654.png)
## 1.3、增强jwt
添加增强类:
~~~java
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> map = new HashMap<>();
// 1. 获取认证信息
// 客户端
String clientId = authentication.getOAuth2Request().getClientId();// 客户端ID
// 用户
Authentication userAuthentication = authentication.getUserAuthentication();
Object principal = userAuthentication.getPrincipal();
if (principal instanceof User){
User user= (User) principal;
map.put("userName", user.getUsername());
}
// 2.设置到accessToken中
map.put("clientId", clientId);
map.put("three", "可添加任何信息");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
return accessToken;
}
}
~~~
### 修改授权服务:
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
@Resource
private DataSource dataSource;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.tokenServices(tokenServices) // 令牌管理
.tokenStore(jwtTokenStore) // 基于jwt管理token
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(tokenEnhancerChain()) // 添加增强jwt
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库中获取客户端信息
clients.withClientDetails(clientDetails());
}
/**
* 从数据库中获取客户端详情
* @return
*/
public ClientDetailsService clientDetails() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
// 配置授权码储存,可存于内存与数据库中
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
// 授权码存于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
// 配置管理令牌服务 ,DefaultTokenServices 和 RemoteTokenServices
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情,从容器中获取
tokenServices.setTokenStore(jwtTokenStore);//基于redis存储令牌,管理token的方式
tokenServices.setTokenEnhancer(tokenEnhancerChain()); //添加增强jwt
// 是否支持 refreshToken
tokenServices.setSupportRefreshToken(true);
// 是否复用 refreshToken
tokenServices.setReuseRefreshToken(false);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24);
//默认30天,这里修改
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
public TokenEnhancerChain tokenEnhancerChain() {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtAccessTokenConverter);
enhancers.add(jwtTokenEnhancer);
//将自定义Enhancer加入EnhancerChain的delegates数组中
enhancerChain.setTokenEnhancers(enhancers);
return enhancerChain;
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
// 授权码存储数据库
return new JdbcApprovalStore(dataSource);
}
}
~~~
![](images/1-52728.png)
## 2、JWT非对称加密:
#### 2.1、生成密钥对
如何生成一个非对称密钥对。这里需要一个密钥对用来配置后面实现的授权服务器和资源服务器。这是一个非对称密钥对(这意味着授权服务器使用它的私钥签署令牌,而资源服务器则使用公钥验证签名)。为了生成该密钥对,这里使用了keytool和OpenSSL,它们是两个简单易用的命令行工具。Java的JDK会安装keytool,所以我们的计算机一般都安装了它。而对于OpenSSL.则需要从官网(https://www.openssl.org/source/)处下载它。如果使用OpenSSL自带的Git Bash,则不需要单独安装它。有了工具之后,需要运行两个命令:在java路径下的bin中找到cmd打开终端,生成文件名为adminrsa.jks
生成一个私钥
获取之前所生成私钥的公钥
命令:
- Keytool -genkeypair -alias adminrsa -keyalg RSA -keypass ad_Min@!123 -keystore adminrsa.jks -storepass ad_Min@!123
- alias 秘钥别名
- keyalg 使用的hash算法
- keypass 秘钥访问密码
- keystore 秘钥库文件名,生成证书文件
- storepass 证书的访问密码
![](images/1-53300.png)
### 2.2、导出公钥:
keytool -list -rfc --keystore adminrsa.jks | openssl x509 -inform pem -pubkey
输入口令,注意不能有换行
![](images/1-53404.png)
复制出来放在某个路径下
### 2.3、修改授权服务
在application.ym配置文件中添加
![](images/1-53452.png)
修改JwtTokenConfig类,如下:
~~~java
@Configuration
public class JwtTokenConfig {
@Value("${privateKey}")
private String privateKey;
@Value("${password}")
private String password;
@Value("${alias}")
private String alias;
@Bean
public TokenStore jwtTokenStore() throws MalformedURLException {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() throws MalformedURLException {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new UrlResource(privateKey),
password.toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
return accessTokenConverter;
}
}
~~~
#### 授权服务类OAuthConfig:
~~~java
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public AuthUserService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private ClientDetailsService clientDetailsService;
@Resource
private DataSource dataSource;
//配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
//配置令牌的访问端点:需要配置认证管理器,授权码服务,令牌管理服务,令牌的请求方式
//这几个都需要以bean的形式进行配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器,密码模式时使用
.authorizationCodeServices(authorizationCodeServices) // 授权码服务,授权码模式时使用,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
.tokenStore(jwtTokenStore) // 基于jwt管理token
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(tokenEnhancerChain()) // 添加增强jwt
.userDetailsService(userDetailsService) // 刷新令牌授权包含对用户信息的检查
.approvalStore(approvalStore()) // 授权记录
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许以POST方式获取令牌
}
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库中获取客户端信息
clients.withClientDetails(clientDetails());
}
/**
* 从数据库中获取客户端详情
* @return
*/
public ClientDetailsService clientDetails() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
// 配置授权码储存,可存于内存与数据库中
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
// 授权码存于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
// 增强jwt
public TokenEnhancerChain tokenEnhancerChain() {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtAccessTokenConverter);
enhancers.add(jwtTokenEnhancer);// jwtTokenEnhancer沿用1.3节的类
//将自定义Enhancer加入EnhancerChain的delegates数组中
enhancerChain.setTokenEnhancers(enhancers);
return enhancerChain;
}
/**
* 使用使用内存方式来保存用户的授权批准记录,也可以使用jdbc保存
* @return
*/
@Bean
public ApprovalStore approvalStore(){
// 授权码存储数据库
return new JdbcApprovalStore(dataSource);
}
}
~~~
### 2.4、修改资源服务
#### 2.4.1、Resource Server
资源服务想要查看 Token 持有的权限,有两种方式:
一种是利用加密算法(对称加密和非对称加密均可),先在授权服务器中(Authorization Server) 使用 私钥 加密 Token,然后在 资源服务器(Resource Server) 中使用 公钥 解密 Token,即可查看到其中的权限(Scope)。
第二种方式就是使用 OAuth2 中规定 Authorization Server 提供的两个端点(Endpoint)接口 : /oauth/check_token 和 UserInfo Endpoint
可在源码中查看 ResourceServerTokenServices 接口及其实现。主要看 DefaultTokenServices、RemoteTokenServices 和 UserInfoTokenServices 这三个实现,下面可看一下这两种方式的配置形式:
##### (1)Jwk加密算法:
```yaml
security:
oauth2:
resource:
jwk:
key-set-uri: http://localhost:8002/oauth/token_key
```
在授权服务配置令牌端点的安全约束中的tokenKeyAccess要放开
```java
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")// 必须放开
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
```
通过改配置,资源服务从授权服务中获取公钥,自己将token解析为明文。
##### (2)Jwt加密算法:
```yaml
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:8002/oauth/token_key
```
其返回的 JSON 只要含有 value 字段,值为对应的公钥,同样令牌端点的安全约束中的tokenKeyAccess要放开,需要依赖授权服务。
或者也可以使用如下的方式直接配置公钥的值:
```yaml
security:
oauth2:
resource:
jwt:
key-value: |
-----BEGIN CERTIFICATE-----
MIIDSTCCArKgAwIBAgIBADANBgkqhkiG9w0BAQQFADB8MQswCQYDVQQGEwJhdzEO
.......NyxMTXzf0=
-----END CERTIFICATE-----
```
##### (3)Jwt KeyStore加密算法:
密钥对存储到了一个 keystore 中,因此也可以直接配置keystore
```yaml
security:
oauth2:
resource:
jwt:
key-store: test.jks
key-store-password: test
key-alias: test
key-password: test
```
##### (4)使用 Check Token Endpoint接口(Authorization Server提供的接口):
```yaml
security:
oauth2:
resource:
token-info-uri: http://localhost:8002/oauth/check_token
prefer-token-info: true
client:
client-id: clients-auth
client-secret: auth3366
```
该方式支持JWT被收回,
在授权服务配置令牌端点的安全约束中的checkTokenAccess要放开
```java
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")// 必须放开
.allowFormAuthenticationForClients();
}
```
##### (5)使用UserInfo Endpoint接口(Authorization Server提供的接口)
```yaml
security:
oauth2:
resource:
user-info-uri: http://localhost:8002/api/user/getCurrentUser
```
由于 UserInfo 接口的验证方式是 Bear Token ,也就是只需要传入对应的 Access Token 即可,所以不需要配置 client_id 和 client_secret。
@ConfigurationProperties(prefix = "security.oauth2.resource")
public class ResourceServerProperties implements BeanFactoryAware, InitializingBean {}
配置获取application.yml中oauth2的参数配置,校验过程
![](images/1-60191.png)
![](images/1-60193.png)
![](images/1-60195.png)
##### 接下来,使用(2)Jwt加密算法的方式来实现,不过公钥使用的是读取文件的方式,不配置在参数中具体如下:
![](images/1-60252.png)
修改资源服务的JwtTokenConfig配置类,如下:
~~~java
@Configuration
public class JwtTokenConfig {
@Value("${pubPath}")
private String pubPath;
//公钥缓存key值,必须唯一
@Value("${redis_key}")
private String PUBLIC_KEY;
@Resource
private RedisTemplate redisTemplate;
@Bean
public TokenStore jwtTokenStore() throws Exception {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() throws Exception {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setVerifierKey(getPublicKey(pubPath));
// accessTokenConverter.setSigningKey("admin-sign");
return accessTokenConverter;
}
/**
* 从redis中获取公钥
*/
public String getPublicKey(String path) throws Exception {
String key = (String) redisTemplate.opsForValue().get(PUBLIC_KEY);
if(StringUtils.isEmpty(key)){
key = RSAUtils.getPubKey(path);
if(!StringUtils.isEmpty(key)){
redisTemplate.opsForValue().set(PUBLIC_KEY,key);
}
}
//String key = RSAUtils.getPubKey(path);
return key;
}
}
~~~
添加读取文件工具类:
~~~java
/**
* 获取公钥
*/
public class RSAUtils {
/**
* 非对称加密公钥Key
*/
public static String getPubKey(String pubPath) throws Exception {
InputStream in = null;
BufferedReader reader = null;
StringBuilder sb = new StringBuilder();
try {
URL url = new URL(pubPath);
URLConnection conn = url.openConnection();
in = conn.getInputStream();
reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}catch (Exception e){
throw new Exception("读取公钥失败");
}finally {
if(null != reader){
reader.close();
}
if(null != in){
in.close();
}
}
}
}
~~~
修改资源服务ResourceServerConfig配置类:
~~~java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.id}")
private String resourceId;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Autowired
private TokenStore jwtTokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authExceptionEntryPoint) // 认证失败时候调用
.resourceId(resourceId) // 必须与认证服务的resourceIds一致,不然就干脆都不配置,ResourceServerSecurityConfigurer默认一个
.tokenStore(jwtTokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 对相关请求进行授权
.anyRequest()// 任意请求
.authenticated()// 都要经过验证
/*.and()
.requestMatchers()// 设置要保护的资源
.antMatchers("/**")*/;// 保护的资源
}
}
~~~
![](images/1-63473.png)
![](images/1-63475.png)
# 十八、Oauth2 + spring cloud gateway
Spring cloud gateway的配置如果不懂,请参考官网:
https://cloud.spring.io/spring-cloud-gateway/reference/html/
对应的中文网址: https://www.springcloud.cc/spring-cloud-greenwich.html#gateway-starter
注意gateway的pom.xml 不要引入 spring-boot-starter-web 这个包,因为会与gateway的webflux冲突
会报:
~~~java
Consider defining a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' in your configuration.
~~~
![](images/1-63477.png)
接下来配置路由,主要做转发:
```yaml
server:
port: 8008
spring:
profiles:
active: test
application:
name: cloud-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #Nacos 链接地址
gateway:
routes:
- id: cloud-oauth
uri: lb://cloud-oauth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: cloud-api
uri: lb://cloud-api
predicates:
- Path=/gateway/api/**
filters:
- StripPrefix=1
```