Spring Security OAuth2.0分布式认证和授权方案
1 OAuth 2.0
1.1 OAuth2.0介绍
-
OAuth (开放授权)是一个开放标准,允许用户授权第三方应用以便访问他们存储在其他服务提供者上的信息,而不需要用户将用户名和密码提供给第三方应用或分享用户数据的所有内容。
-
OAuth 2.0是OAuth 协议的延续版本,但是不兼容OAuth 1.0即完全废除了OAuth 1.0。很多大公司如Google、Microsoft等都提供了OAuth 认证服务,这些都足以说明OAuth 标准逐渐成为开放资源授权的标准。
-
下边分析一个OAuth 2.0认证的例子,通过这个例子可以很好的理解OAuth 2.0协议的认证过程,本例子是借助ProcessOn网站使用QQ认证的过程,过程简要描述如下:
-
用户借助QQ认证登录ProcessOn网站,用户就不需要在ProcessOn网站上单独注册用户,那么怎么才算认证成功呢?ProcessOn网站需要成功从QQ获取用户的身份信息则认为用户认证成功,那如何从QQ获取用户的身份信息?用户信息的拥有者是用户本人,QQ需要经过用户的同意方可为ProcessOn网站生成令牌,ProcessOn拿到此令牌才可以从QQ获取用户的信息。
-
1️⃣客户端请求第三方授权:用户进入ProcessOn的登录页面,点击QQ的图标以QQ的账号和密码登录系统,用户是自己在QQ里信息的资源拥有者。
- 2️⃣资源拥有者同意给客户端授权:资源拥有者输入QQ的账号和密码表示资源拥有者同意给客户端授权,QQ会对资源拥有者的身份进行验证,验证通过后,QQ会询问用户是否给ProcessOn访问自己的QQ数据,用户打开QQ手机版,点击“确认登录”表示同意授权,QQ认证服务器会颁发一个授权码,并重定向到ProcessOn的网站。
- 3️⃣客户端获取到授权码,请求认证服务器申请令牌:此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
- 4️⃣认证服务器向客户端响应令牌:QQ认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户是看不到的,当客户端拿到令牌后,用户在ProcessOn网站上看到已经登录成功。
- 5️⃣客户端请求资源服务器的资源:客户端携带令牌请求访问QQ服务器获取用户的基本信息。
- 6️⃣资源服务器返回受保护资源:资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
1.2 OAuth 2.0角色
- 1️⃣客户端:本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端、微信客户端等。
- 2️⃣资源拥有者:通常为用户,也可以是应用程序,即该资源的拥有者。
- 3️⃣授权服务器(也称为认证服务器):用于服务提供商对资源拥有的身份进行认证,对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭证。
- 4️⃣资源服务器:存储资源的服务器,比如QQ服务器。
服务提供商不会随便允许任意一个客户端接入到它的授权服务器的,服务提供商会给准入的接入方一个身份,用于接入的凭证:client_id(客户端标识)和client_secret(客户端密钥)。
1.3 OAuth 2.0的四种授权方式
1.3.1 概述
- OAuth 2.0规定了四种授权模式。
- 1️⃣授权码(authorization code)。
- 2️⃣隐藏式(implicit)。
- 3️⃣密码(password)。
- 4️⃣客户端凭证(client credentials)。
1.3.2 授权码(authorization code)
-
授权码方式,指的是第三方应用先申请一个授权码,然后再用这个授权码去获取令牌。
-
授权码方式是最常用的方式,安全性也最高,它适用于那些有后端的Web应用。授权码通过前端发送,令牌是存储到后端的,而且所有和资源服务器的通信都是在后端完成。这样的前后端分离,可以避免令牌泄露。
-
1️⃣A网站提供一个链接,用户点击后就会跳转到B网站,授权用户数据给A网站使用。下面就是A网站跳转到B网站的一个示例连接。
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面的URL中,
response_type
参数表示要求返回的授权码code
,client_id
参数是让B网站知道是谁在请求,redirect_uri
参数是B接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)。
- 2️⃣用户点击后,B网站会要求用户登录,然后询问是否同意给A网站授权。用户表示同意,这时B网站就会调回redirect_uri参数指定的网址。跳转的时候,会传一个授权码。
https://a.com/callback?code=AUTHORIZATION_CODE
上面的URL中,
code
参数就是授权码。
- 3️⃣A网站拿到授权码以后,可以在后端向B网站请求令牌。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=AUTHORIZATION_CODE&
code=authorization_code&
redirect_uri=CALLBACK_URL
上面的URL中,
client_id
参数和client_secret
参数用来让B网站确认A网站的身份(client_secret参数是保密的,所以只能由后端发送请求)。grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。
- 4️⃣B网站收到请求以后,就会颁发令牌。具体的做法是向
redirect_uri
指定的网址,发送一段JSON数据,
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
上面的JSON数据中,
access_token
字段就是令牌,A网站在后端可以拿到。
1.3.3 隐藏式(implicit)
-
有些web应用是纯前端应用,没有后端。这时就不能使用上面的方式了,必须将令牌存储在前端。这种方式没有授权码这个步骤,所以称为授权码的隐藏式。
-
1️⃣A网站提供一个连接,要求用户跳转到B网站,授权用户数据给A网站使用。
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面的URL中,
response_type
参数为token
,表示要求直接返回令牌。
- 2️⃣用户点击后,跳转到B网站,登录后同意给A网站授权。这时,B网站就会跳转到
redirect_uri
参数指定的跳转网址,并且把令牌作为URL参数,传给A网站。
https://a.com/callback#token=ACCESS_TOKEN
上面的URL中,
token
的参数就是令牌,A网站因此直接在前端拿到令牌。注意:令牌的位置是URL锚点,而不是查询字符串,这是因为OAuth 2.0允许跳转网址是HTTP协议,因为存在“中间人攻击”的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄露令牌的风险。
隐藏式是把令牌直接传给前端,很不安全。因此,只能用于一些安全性要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间有效,浏览器关闭,令牌就失效了。
1.3.4 密码式(password)
-
如果你高度信任某个应用,OAuth 2.0允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为密码式。
-
1️⃣A网站要求用户提供B网站的用户名和密码。拿到以后,A直接向B请求令牌。
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
上面的URL中,
grant_type
参数是授权方式,这里的password
表示密码式,username
和password
是B的用户名和密码。
- 2️⃣B网站验证身份通过后,直接给出令牌。注意,此时不需要跳转,而是把令牌放在JSON里面,作为HTTP回应,A网站因此拿到令牌。
这种方式需要用户给出自己的用户名和密码,风险非常大,因此只适用于其他授权都无法采用的情况,而且必须是用户高度信任的应用。
1.3.5 客户端凭证式(client credentials)
-
这种方式适用于没有前端的命令行应用,即在命令行下请求令牌。
-
1️⃣A应用在命令行向B发出请求。
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
上面的URL中,
grant_type
参数是授权方式,这里的client_credentials
表示客户端凭证式,client_id
和client_secret
用来让B确认A的身份。
- 2️⃣B网站验证通过后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
1.4 OAuth 2.0中令牌的使用
- A网站拿到令牌以后,就可以向B网站的API请求数据了。
- 每个发送的API请求,都必须携带令牌。具体的做法就是在请求的头信息,加上Authorization字段,令牌就放在这个字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"
上面命令中,
Authorization
就是拿到的令牌。
1.5 OAuth 2.0中更新令牌
- 令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,可能体验不好,而且也没有必要。OAuth 2.0允许用户自动更新令牌。
- B网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token)。令牌到期后,用户使用refresh token发送一个请求,去更新令牌。
https://b.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
上面的URL中,
grant_type
参数为refresh_token
表示要求更新令牌,client_id
参数和client_secret
参数用于确认身份,refresh_token
参数就是用于更新令牌的令牌。
- B网站验证通过后,就会颁发新的令牌了。
2 Spring Security OAuth2
2.1 介绍
-
Spring Security OAuth2是对OAuth2的一种实现。
-
OAuth2的服务提供涵盖了两种服务:授权服务(Authorization Server,认证和授权服务)和资源服务(Resource Server)。使用Spring Security OAuth2的时候可以将授权服务和资源服务放在同一个应用程序中试下你,也可以选择建立使用同一个授权服务的多个资源服务。
-
授权服务
(Authorization Server):包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌的请求端点由Spring MVC或Spring Webflux的控制器进行实现,下面是配置一个认证服务要实现的endpoints(端点):AuthorizationEndpoint
用于认证请求。默认的URL是/oauth/authorize。TokenEndpoint
用于访问令牌的请求。默认的URL是/oauth/token。
-
资源服务
(Resource Server):包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等,下面的过滤器用于实现OAuth2的资源服务。- OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。
2.2 环境搭建
2.2.1 mysql数据库的脚本
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;
-- ----------------------------
-- Records of oauth_approvals
-- ----------------------------
INSERT INTO `oauth_approvals` VALUES ('admin', 'client_id', 'write', 'APPROVED', '2020-12-11 13:36:21', '2020-11-11 13:36:21');
INSERT INTO `oauth_approvals` VALUES ('admin', 'client_id', 'read', 'APPROVED', '2020-12-11 13:36:21', '2020-11-11 13:36:21');
-- ----------------------------
-- 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 ('client_id', 'product-api', '$2a$10$opFZ09sDtJ7yEA65V8cvnOcxgaWT9sqxy5TNzahQvvnhrsMgQQNVy', 'read,write', 'authorization_code,password,implicit,client_credentials,refresh_token', 'http://localhost:9001', 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', '$2a$10$L0RfoLUKcx/a0HHW9p2/WOZQDa8NdTOv463TCXsaXT0wx7uJT0wjq', 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;
2.2.2 父工程的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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>spring-security-oauth2-uaa</module>
<module>spring-security-oauth2-product</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.11.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.sunxiaping</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>1.0</version>
<name>spring-security-oauth2</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.11.RELEASE</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>0.0.25</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/Abstract*.java</exclude>
</excludes>
<systemPropertyVariables>
<java.security.egd>file:/dev/./urandom</java.security.egd>
<java.awt.headless>true</java.awt.headless>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-rules</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>commons-logging:*:*</exclude>
</excludes>
<searchTransitive>true</searchTransitive>
</bannedDependencies>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
<inherited>true</inherited>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
2.2.3 uua授权服务工程的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">
<parent>
<artifactId>spring-security-oauth2</artifactId>
<groupId>com.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-uaa</artifactId>
<description>认证的微服务</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
</project>
2.2.4 product资源服务工程的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">
<parent>
<artifactId>spring-security-oauth2</artifactId>
<groupId>com.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-product</artifactId>
<description>资源微服务</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
</project>
2.3 授权服务器配置
2.3.1 @EnableAuthorizationServer
- 可以使用@EnableAuthorizationServer注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0授权服务器。
- 在config包下创建Oauth2ServerConfig:
package com.sunxiaping.config;
import com.sunxiaping.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
}
- AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类由Spring创建的独立的配置对象,它们会被Spring传入AuthorizationServerConfigurer中进行配置
package org.springframework.security.oauth2.config.annotation.web.configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {
}
//用来配置令牌端点的安全性约束
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
//ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
//用来配置令牌(token)的访问端点和令牌服务(token services)
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
2.3.2 配置客户端详情服务
- ClientDetailsServiceConfigurer能够适用内存或者JDBC来实现客户端详情服务(ClientDetailsService),ClientDetailsService负责查找ClientDetails,而ClientDetails有以下几个重要的属性如下表所示:
属性 | 描述 |
---|---|
clientId | 用来标识客户端的id。 |
secret | 客户端安全码,需要是信任的客户端。 |
scope | 用来限制客户端的访问范围,如果为空(默认),那么客户端将拥有全部的访问范围 |
authorizationGrantTypes | 此客户单可以使用的授权类型,默认为空。 |
authorities | 此客户端可以使用的权限(基于Spring Security authorities) |
- 客户端详情(ClientDetails)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系型数据库的表中,就可以使用JdbcClientDetailsService)或者通过自己实现ClientRegistrationService接口(同时也可以使用ClientDetailsService接口)来进行管理。
- 暂时使用内存方式存储客户端详情信息,配置如下:
package com.sunxiaping.config;
import com.sunxiaping.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetaisService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//使用内存方式存储客户端详情
clients.inMemory()
.withClient("client_id") //client_id
.secret(passwordEncoder.encode("secret")) //client_secret
.resourceIds("product-id") //资源标识
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //该client允许的授权类型
.scopes("read,write")//允许的授权范围
.autoApprove(false)
.resourceIds("https://www.baidu.com"); //回调地址
}
}
2.3.3 令牌服务
-
AuthorizationServerTokenServices接口定义了一些操作使得我们可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。
-
我们自己可以创建AuthorizationServerTokenServices的实现类,但是我们也可以继承DefaultTokenServices这个类,因为DefaultTokenServices里面包含了一些有用的实现,我们可以使用它来修改令牌的格式和令牌的存储。默认情况下,当DefaultTokenServices创建一个令牌的时候,是使用随机数来进行填充的,除了持久化令牌是委托给一个TokenStore接口来实现以外,这个类几乎帮助我们做了所有的事情。并且TokenStore接口还有一个默认的实现,它就是InMemoryTokenStore,表明所有的令牌都保存在内存中。除了使用InMemoryTokenStore外,我们还可以使用其他的预定义实现,它们都实现了TokenStore接口:
- 1️⃣InMemoryTokenStore:这个实现是默认采用的,它可以完美的工作的单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个类来进行尝试,我们可以在开发的时候使用这个类进行管理,因为不会被保存到磁盘中,更易于调试。
- 2️⃣JdbcTokenStore:这是一个基于JDBC的实现,令牌会被保存进关系型数据库。使用这个类的时候,我们可以在不同的服务器之间共享令牌信息。
- 3️⃣JwtTokenStore:这是一个基于Jwt的实现,它可以把令牌相关的数据进行编码(对后端服务来说,它不需要进行存储,有很大的优势),但是有一个缺点,就是撤销一个已经授权令牌会非常困难,所以通常用来处理一个生产周期较短的令牌以及撤销刷新令牌(refresh token)。另一个缺点就是这个令牌的占用空间非常大。
-
我们暂时先使用InMemoryTokenStore方式生成普通的令牌,并配置令牌服务:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
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.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* Token存储策略,使用内存方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//使用内存方式存储客户端详情
clients.inMemory()
.withClient("client_id") //client_id
.secret(passwordEncoder.encode("secret")) //client_secret
.resourceIds("product-id") //资源标识
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //该client允许的授权类型
.scopes("read,write")//允许的授权范围
.autoApprove(false) //false表示跳转到授权页面
.redirectUris("https://www.baidu.com"); //回调地址
}
}
2.3.4 令牌访问端点配置
-
AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌endpoint的配置。
-
配置授权类型(Grant Types)
:AuthorizationServerEndpointsConfigurer通过设定如下的属性来决定支持的授权支持的授权类型(Grant Types)。- authenticationManager:认证管理器,当我们选择了资源所有者的密码授权类型的时候,需要为这个属性注入一个AuthenticationManager对象。
- userDetailsService:如果我们设置了这个属性的话,说明我们有一个自己的UserDetailsService接口的实现。
- authorizationCodeServices:这个属性是用来设置授权码服务的(即AuthorizationCodeServices的实例对象),主要用于“authorization_code”授权码类型模式。
- implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
- tokenGranter:当我们设置了这个属性(即tokenGranter接口实现),那么授权将完全由我们自己来掌控,并且会忽略上面的几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了我们的需求,才会考虑使用这个,实际中一般不用。
-
配置授权端点的URL(Endpoint URLs)
:-
AuthorizationServerEndpointsConfigurer有一个叫做pathMapping()的方法用来配置端点URL链接,它有两个参数。
- 第一个参数:String类型,这个端点URL的默认链接。
- 第二个参数:String类型,我们可以进行替代的URL链接。
-
以上的参数都将以"/"字符为开始的字符串,框架的默认URL链接如下所示,可以作为这个pathMapping()方法的第一个参数:
URL链接 描述 /oauth/authorize 授权端点 /oauth/token 令牌端点 /oauth/confirm_access 用户确认授权提交端点 /oauth/error 授权服务错误信息端点 /oauth/check_token 用于资源服务访问的令牌解析端点 /oauth/token_key 提供公共密钥的端点,如果我们使用JWT令牌的话
-
-
下面配置SpringSecurity(这个和OAuth2没有太多必然联系):
- application.yml
server: port: 9001 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root password: 123456 # Hikari 连接池配置 hikari: # 最小空闲连接数量 minimum-idle: 5 # 空闲连接存活最大时间,默认600000(10分钟) idle-timeout: 180000 # 连接池最大连接数,默认是10 maximum-pool-size: 1000 # 此属性控制从池返回的连接的默认自动提交行为,默认值:true auto-commit: true # 连接池名称 pool-name: HikariCP # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 max-lifetime: 1800000 # 数据库连接超时时间,默认30秒,即30000 connection-timeout: 30000 connection-test-query: SELECT 1 data-source-properties: useInformationSchema: true main: # 允许我们自己覆盖Spring放入到IOC容器的对象 allow-bean-definition-overriding: true mybatis: type-aliases-package: com.sunxiaping.domain configuration: map-underscore-to-camel-case: true logging: level: com.sunxiaping: debug
- SysUser.java
package com.sunxiaping.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; public class SysUser implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List<SysRole> roles; public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; } }
- SysRole.java
package com.sunxiaping.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; public class SysRole implements GrantedAuthority { private Integer id; private String roleName; private String roleDesc; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public String getRoleDesc() { return roleDesc; } public void setRoleDesc(String roleDesc) { this.roleDesc = roleDesc; } @JsonIgnore @Override public String getAuthority() { return roleName; } }
- UserMapper.java
package com.sunxiaping.mapper; import com.sunxiaping.domain.SysUser; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface UserMapper { @Select("select * from sys_user where username = #{username}") @Results( {@Result(id = true, property = "id", column = "id"), @Result(property = "roles", column = "id", javaType = List.class, many = @Many(select = "com.sunxiaping.mapper.RoleMapper.findByUid"))}) SysUser findByName(String username); }
- RoleMapper.java
package com.sunxiaping.mapper; import com.sunxiaping.domain.SysRole; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface RoleMapper { @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " + "FROM sys_role r, sys_user_role ur " + "WHERE r.id=ur.rid AND ur.uid=#{uid}") List<SysRole> findByUid(Integer uid); }
- UserService.java
package com.sunxiaping.service; import org.springframework.security.core.userdetails.UserDetailsService; public interface UserService extends UserDetailsService { }
- UserServiceImpl.java
package com.sunxiaping.service.impl; import com.sunxiaping.mapper.UserMapper; import com.sunxiaping.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return userMapper.findByName(s); } }
- 启动类:
package com.sunxiaping; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 许大仙 * @version 1.0 * @since 2020-11-09 17:03 */ @SpringBootApplication @MapperScan(basePackages = "com.sunxiaping.mapper") public class UaaApplication { public static void main(String[] args) { SpringApplication.run(UaaApplication.class, args); } }
- WebSecurityConfig.java
package com.sunxiaping.config; import com.sunxiaping.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 许大仙 * @version 1.0 * @since 2020-11-09 17:25 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 所有资源必须授权后访问 .anyRequest().authenticated() .and() .formLogin().loginProcessingUrl("/login").permitAll()// 指定认证页面可以匿名访问 // 关闭跨站请求防护 .and().csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/favicon.ico", "*/v2/api-docs", "*/v3/api-docs", "*/webjars/**", "/*/api-docs", "/swagger**/**", "/doc.html", "/v3/**"); } /** * AuthenticationManager对象在Oauth2认证服务中要使用,提前放入到IOC容器中 * * @return * @throws Exception */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
-
配置令牌访问和令牌服务部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* Token存储策略,使用内存方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* 设置授权码模式的授权服务,此处使用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
/**
* 配置令牌(token)的访问端点和令牌服务(token Services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) // UserDetailsService
.authorizationCodeServices(authorizationCodeServices()) //授权服务
.tokenServices(authorizationServerTokenServices()) //令牌管理服务
.approvalStore(approvalStore());
}
}
- 配置客户端详情服务和配置令牌访问和令牌服务完整部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* Token存储策略,使用内存方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* 设置授权码模式的授权服务,此处使用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//使用内存方式存储客户端详情
clients.inMemory()
.withClient("client_id") //client_id
.secret(passwordEncoder.encode("secret")) //client_secret
.resourceIds("product-id") //资源标识
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //该client允许的授权类型
.scopes("read,write")//允许的授权范围
.autoApprove(false) //false表示跳转到授权页面
.redirectUris("https://www.baidu.com"); //回调地址
}
/**
* 配置令牌(token)的访问端点和令牌服务(token Services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) // UserDetailsService
.authorizationCodeServices(authorizationCodeServices()) //授权服务
.tokenServices(authorizationServerTokenServices()) //令牌管理服务
.approvalStore(approvalStore());
}
}
2.3.5 令牌端点的安全性约束
- AuthorizationServerSecurityConfigurer用来配置令牌端点的安全约束。
- 配置令牌端点的安全性约束部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 用来配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //当使用JwtToken且使用非对称加密时,资源服务用语获取公钥而开放的,这里配置的是这个端点完全开放
.checkTokenAccess("permitAll()") //checkToken这个端点完全公开
.allowFormAuthenticationForClients(); //允许表单认证
}
}
- 配置客户端详情服务和配置令牌访问和令牌服务完整部分以及配置令牌端点的安全性约束完整部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* Token存储策略,使用内存方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* 设置授权码模式的授权服务,此处使用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
@Autowired
private PasswordEncoder passwordEncoder;
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//使用内存方式存储客户端详情
clients.inMemory()
.withClient("client_id") //client_id
.secret(passwordEncoder.encode("secret")) //client_secret
.resourceIds("product-id") //资源标识
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //该client允许的授权类型
.scopes("read,write")//允许的授权范围
.autoApprove(false) //false表示跳转到授权页面
.redirectUris("https://www.baidu.com"); //回调地址
}
/**
* 配置令牌(token)的访问端点和令牌服务(token Services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) // UserDetailsService
.authorizationCodeServices(authorizationCodeServices()) //授权服务
.tokenServices(authorizationServerTokenServices()) //令牌管理服务
.approvalStore(approvalStore());
}
/**
* 用来配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //当使用JwtToken且使用非对称加密时,资源服务用语获取公钥而开放的,这里配置的是这个端点完全开放
.checkTokenAccess("permitAll()") //checkToken这个端点完全公开
.allowFormAuthenticationForClients(); //允许表单认证
}
}
2.3.6 授权服务配置总结
- 1️⃣要完成认证,首先需要知道客户端信息从哪里读取,因此需要配置客户端详情服务。
- 2️⃣要颁发token,必须定义token相关的端点以及token如何存储,客户端支持哪些类型的token。
- 3️⃣因为要暴露一些端点,那么需要对这些端点进行一些安全上的约束。
2.4 资源服务器配置
-
配置SpringSecurity的相关配置:
- application.yml
server: port: 9002 spring: main: # 允许我们自己覆盖Spring放入到IOC容器的对象 allow-bean-definition-overriding: true logging: level: com.sunxiaping: debug
- ProductController.java
package com.sunxiaping.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/product") public class ProductController { @Secured("ROLE_ADMIN") @GetMapping(value = "/findAll") public String findAll() { return "产品列表查询成功"; } }
- ProductApplication.java
package com.sunxiaping; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }
- WebSecurityConfig.java
package com.sunxiaping.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @author 许大仙 * @version 1.0 * @since 2020-11-12 16:25 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 所有资源必须授权后访问 .anyRequest().authenticated() // 关闭跨站请求防护 .and().csrf().disable(); } }
-
配置资源服务器
- Oauth2SourceConfig.java
package com.sunxiaping.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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; import org.springframework.security.oauth2.provider.token.RemoteTokenServices; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; /** * 资源配置 * * @author 许大仙 * @version 1.0 * @since 2020-11-09 16:42 */ @Configuration @EnableResourceServer // 开启资源服务器 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter { /** * 指定当前资源的id和存储方案 * * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("product-id") // 指定当前资源的id .tokenServices(tokenService());// 指定保存token的方式 } // 资源服务令牌解析服务 @Bean public ResourceServerTokenServices tokenService() { //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret RemoteTokenServices service = new RemoteTokenServices(); service.setCheckTokenEndpointUrl("http://localhost:9001/oauth/check_token"); service.setClientId("client_id"); service.setClientSecret("secret"); return service; } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。 .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read,write')") .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')") .and() .headers() .addHeaderWriter((request, response) -> { response.addHeader("Access-Control-Allow-Origin", "*");// 允许跨域 if (request.getMethod().equals("OPTIONS")) {// 如果是跨域的预检请求,则原封不动向下传达请求头信息 response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access- Control-Request-Method")); response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access- Control-Request-Headers")); } }); } }
-
测试访问资源:
2.5 测试
2.5.1 授权码模式测试
- 通过浏览器访问下面的地址:
http://localhost:9001/oauth/authorize?client_id=client_id&response_type=code
- 第一次访问会跳转到登录页面:
- 输入用户名和密码验证成功后,会询问用户是否授权:
- 选择授权后会跳转到指定的地址(我在授权服务配置的是百度的网址),浏览器地址上会包含一个授权码:
- 根据授权码向服务器申请令牌:
- 验证token:
2.5.2 隐藏式模式测试
- 通过浏览器访问下面的地址:
http://localhost:9001/oauth/authorize?client_id=client_id&response_type=token
- 第一次访问会跳转到登录页面:
- 输入用户名和密码验证成功后,会询问用户是否授权:
- 选择授权后会跳转到指定的地址(我在授权服务配置的是百度的网址),浏览器地址上会包含一个令牌:
2.5.3 密码模式测试
- 通过client_id、client_secret、username、password以及grant_type=password向授权服务器申请令牌:
curl http://localhost:9001/oauth/token -X POST -d "grant_type=password&client_id=client_id&client_secret=secret&username=admin&password=123456"
2.5.4 客户端凭证式测试
- 通过client_id、client_secret以及grant_type=client_credentials向授权服务器申请令牌:
curl http://localhost:9001/oauth/token -X POST -d "grant_type=client_credentials&client_id=client_id&client_secret=secret"
2.6 JWT
2.6.1 前言
- 通过上面的测试我们发现,当资源服务和授权服务不在一起的时候,资源服务使用RemoteTokenServices远程请求授权服务验证token,如果访问量较大的时候会影响系统的性能。
- 那么如何解决上面的问题,就是令牌采用JWT的格式。用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定好的算法自动完成令牌校验,无需每次都请求认证任务完成授权。
2.6.2 什么是JWT?
- JWT(JSON Web Token)是一个开放的行业标准(RFC 7519),它定义了一种简单的、自包含的协议格式,用于在通信双方传递JSON对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
- JWT的官网和标准。
- JWT的优点:
- JWT基于JSON,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法以及数字签名技术,JWT防止篡改,安全性高。
- 资源服务使用JWT可以不依赖认证服务即可完成授权。
- JWT的缺点:
- JWT的令牌较长,占存储空间比较大。
2.6.3 JWT的令牌结构
-
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如xxx.yyy.zzz。
-
Header:
- 头部包含令牌的类型(即JWT)以及使用的哈希算法(如RSA等)。
- 例如:
{"alg":"HS256","typ":"JWT"}
- 将上面的内容使用Base64Url编码,得到的一个字符串就是JWT令牌的第一个部分。
-
Payload:
- 第二部分是有效载荷,内容也是一个JSON对象,它是存放有效信息的地方,它可以存放JWT提供的现成字段,比如:iss(签发者)、exp(过期时间戳)、sub(面向的用户)等,也可以自定义字段。
- 此部分不建议存放敏感信息,因此此部分可以解码还原原始内容。
- 例如:
{"sub":"1234567890","name":"456","admin":true}
- 将上面的内容使用Base64Url编码,得到的一个字符串就是JWT令牌的第二个部分。
-
Signature:
-
第三部分是签名,此部分用于防止JWT内容被篡改。
-
这个部分使用Base64Url将前两部分进行编码,编号后使用点(.)连接组成字符串,最后使用header中声明的算法进行签名。
-
例如:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
-
2.6.4 配置JWT令牌服务
- 在uaa中配置JWT令牌服务,即可实现生成jwt格式的令牌。
- Oauth2ServerConfig.java的修改部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.Arrays;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
String SIGNING_KEY = "admin";
/**
* Token存储策略,使用JWT的方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //对称密钥,资源服务器使用该密钥来验证
return jwtAccessTokenConverter;
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
//对令牌服务进行增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
//其他略
}
- Oauth2ServerConfig.java的完整部分:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.Arrays;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
String SIGNING_KEY = "admin";
/**
* Token存储策略,使用JWT的方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //对称密钥,资源服务器使用该密钥来验证
return jwtAccessTokenConverter;
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
//对令牌服务进行增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* 设置授权码模式的授权服务,此处使用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
@Autowired
private PasswordEncoder passwordEncoder;
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//使用内存方式存储客户端详情
clients.inMemory()
.withClient("client_id") //client_id
.secret(passwordEncoder.encode("secret")) //client_secret
.resourceIds("product-id") //资源标识
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //该client允许的授权类型
.scopes("read,write")//允许的授权范围
.autoApprove(false) //false表示跳转到授权页面
.redirectUris("https://www.baidu.com"); //回调地址
}
/**
* 配置令牌(token)的访问端点和令牌服务(token Services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) // UserDetailsService
.authorizationCodeServices(authorizationCodeServices()) //授权服务
.tokenServices(authorizationServerTokenServices()); //令牌管理服务
// .approvalStore(approvalStore());
}
/**
* 用来配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //当使用JwtToken且使用非对称加密时,资源服务用语获取公钥而开放的,这里配置的是这个端点完全开放
.checkTokenAccess("permitAll()") //checkToken这个端点完全公开
.allowFormAuthenticationForClients(); //允许表单认证
}
}
2.6.5 生成JWT令牌
2.6.6 校验JWT令牌
- 资源服务需要和授权服务拥有一致的签名、令牌服务等。
- Oauth2SourceConfig.java的修改部分:
package com.sunxiaping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 资源配置
*
* @author 许大仙
* @version 1.0
* @since 2020-11-09 16:42
*/
@Configuration
@EnableResourceServer // 开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter {
String SIGNING_KEY = "admin";
/**
* Token存储策略,使用JWT的方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //对称密钥,资源服务器使用该密钥来验证
return jwtAccessTokenConverter;
}
/**
* 指定当前资源的id和存储方案
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("product-id") // 指定当前资源的id
.tokenStore(tokenStore());//
}
//略
}
- Oauth2SourceConfig.java的完整部分:
package com.sunxiaping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 资源配置
*
* @author 许大仙
* @version 1.0
* @since 2020-11-09 16:42
*/
@Configuration
@EnableResourceServer // 开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter {
String SIGNING_KEY = "admin";
/**
* Token存储策略,使用JWT的方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //对称密钥,资源服务器使用该密钥来验证
return jwtAccessTokenConverter;
}
/**
* 指定当前资源的id和存储方案
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("product-id") // 指定当前资源的id
.tokenStore(tokenStore());//
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read,write')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
.and()
.headers()
.addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");// 允许跨域
if (request.getMethod().equals("OPTIONS")) {// 如果是跨域的预检请求,则原封不动向下传达请求头信息
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access- Control-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access- Control-Request-Headers"));
}
});
}
}
- 测试:使用JWT令牌请求资源
2.7 完善环境配置
-
上面的客户端信息和授权码依然存储在内存中,生产环境中通常会存储在数据库中。
-
修改Oauth2ServerConfig.java中的ClientDetailsService和AuthorizationCodeServices使其从数据库中读取:
package com.sunxiaping.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
import java.util.Arrays;
/**
* @author 许大仙
* @version 1.0
* @since 2020-11-09 17:17
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
String SIGNING_KEY = "admin";
@Autowired
private DataSource dataSource;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/**
* Token存储策略,使用JWT的方式
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //对称密钥,资源服务器使用该密钥来验证
return jwtAccessTokenConverter;
}
@Bean
public ClientDetailsService clientDetailsService() {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 设置授权码模式的授权服务,此处使用数据库方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
/**
* 令牌服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService()); //客户端详情服务
defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
defaultTokenServices.setTokenStore(tokenStore());//token存储策略
//对令牌服务进行增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默认的有效期是2小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
return defaultTokenServices;
}
/**
* ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService).
* 客户端详情信息在这里进行初始化,我们可以把客户端详情信息写死在这里,也可以通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
/**
* 配置令牌(token)的访问端点和令牌服务(token Services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) // UserDetailsService
.authorizationCodeServices(authorizationCodeServices()) //授权服务
.tokenServices(authorizationServerTokenServices()) //令牌管理服务
.approvalStore(approvalStore());
}
/**
* 用来配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //当使用JwtToken且使用非对称加密时,资源服务用语获取公钥而开放的,这里配置的是这个端点完全开放
.checkTokenAccess("permitAll()") //checkToken这个端点完全公开
.allowFormAuthenticationForClients(); //允许表单认证
}
}