系列導航
SpringSecurity系列
- SpringSecurity系列學習(一):初識SpringSecurity
- SpringSecurity系列學習(二):密碼驗證
- SpringSecurity系列學習(三):認證流程和源碼解析
- SpringSecurity系列學習(四):基於JWT的認證
- SpringSecurity系列學習(四-番外):多因子驗證和TOTP
- SpringSecurity系列學習(五):授權流程和源碼分析
- SpringSecurity系列學習(六):基於RBAC的授權
SpringSecurityOauth2系列
- SpringSecurityOauth2系列學習(一):初認Oauth2
- SpringSecurityOauth2系列學習(二):授權服務
- SpringSecurityOauth2系列學習(三):資源服務
- SpringSecurityOauth2系列學習(四):自定義登陸登出接口
- SpringSecurityOauth2系列學習(五):授權服務自定義異常處理
授權服務
認識了Oauth2之后,接下來我們就要開始正式編碼寫一個demo了
從上一節學習中看來,授權服務起了非常大的重用,我們這里也從搭建一個授權服務開始
在動手之前,咱們來了解一下SpringSecurityOauth2中授權服務的一些概念
概念
授權服務配置
- 配置一個授權服務,需要考慮
授權類型(GrantType)、不同授權類型為客戶端(Client)
提供了不同的獲取令牌(Token)
方式,每一個客戶端(Client)
都能夠通過明確的配置以及權限來實現不同的授權訪問機制,也就是說如果你提供了一個client_credentials
授權方式,並不意味着其它客戶端就要采用這種方式來授權 - 使用
@EnableAuthorizationServer
來配置授權服務機制,並繼承AuthorizationServerConfigurerAdapter
該類重寫configure
方法定義授權服務器策略
配置客戶端詳情(Client Details)
ClientDetailsServiceConfigurer
能夠使用內存或 JDBC 方式實現獲取已注冊的客戶端詳情,有幾個重要的屬性:- clientId:客戶端標識 ID(賬號)
- secret:客戶端安全碼(密碼)
- scope:客戶端訪問范圍,默認為空則擁有全部范圍
- authorizedGrantTypes:客戶端使用的授權類型,默認為空
- authorities:客戶端可使用的權限
管理令牌(Managing Token)
讀和寫令牌所用的tokenService
不同
ResourceServerTokenServices
接口定義了令牌加載、讀取方法AuthorizationServerTokenServices
接口定義了令牌的創建、獲取、刷新方法ConsumerTokenServices
定義了令牌的撤銷方法(刪除)DefaultTokenServices
實現了上述三個接口,它包含了一些令牌業務的實現,如創建令牌、讀取令牌、刷新令牌、獲取客戶端ID。默認的創建一個令牌時,是使用 UUID 隨機值進行填充的。除了持久化令牌是委托一個TokenStore
接口實現以外,這個類幾乎幫你做了所有事情- 而
TokenStore
接口負責持久化令牌,也有一些實現:InMemoryTokenStore
:默認采用該實現,將令牌信息保存在內存中,易於調試JdbcTokenStore
:令牌會被保存近關系型數據庫,可以在不同服務器之間共享令牌JwtTokenStore
:使用 JWT 方式保存令牌,它不需要進行存儲,但是它撤銷一個已經授權令牌會非常困難,所以通常用來處理一個生命周期較短的令牌以及撤銷刷新令牌RedisTokenStore
:保存在Redis中,這種方式比起JDBC的優點在於redis的性能高,讀取和寫入非常快,缺點就是需要一個多維護一個reids中間件環境。
JWT 令牌(JWT Tokens)
- 使用 JWT 令牌需要在授權服務中配置一個
tokenEnhancer
去簽發JWT令牌,JWT的簽發與驗證 依賴這個類進行編碼以及解碼,因此授權服務需要這個轉換類,並為資源服務器提供密鑰(對稱加密)或者公鑰(非對稱加密)對JWT進行解密 - Token 令牌默認是有簽名的,並且資源服務器中需要驗證這個簽名,因此需要一個對稱的 Key 值,用來參與簽名計算
- 這個 Key 值存在於授權服務之中,或者使用非對稱加密算法加密 Token 進行簽名,Public Key 公布在 jwk set這個端點接口中,需要自行實現
配置授權類型(Grant Types)
- 授權是使用
AuthorizationEndpoint
這個端點來進行控制的,使用AuthorizationServerEndpointsConfigurer
這個對象實例來進行配置,它可配置以下屬性:authenticationManager
:認證管理器,當你選擇了資源所有者密碼(password)授權類型的時候,請設置這個屬性注入一個AuthenticationManager
對象userDetailsService
:可定義自己的UserDetailsService
接口實現authorizationCodeServices
:用來設置收取碼服務的(即 AuthorizationCodeServices 的實例對象),主要用於authorization_code
授權碼類型模式implicitGrantService
:這個屬性用於設置隱式授權模式,用來管理隱式授權模式的狀態
tokenGranter
:完全自定義授權服務實現(TokenGranter 接口實現),只有當標准的四種授權模式已無法滿足需求時才需要自行去實現。
配置授權端點 URL(Endpoint URLs)
-
AuthorizationServerEndpointsConfigurer
配置對象有一個pathMapping()
方法用來配置端點的 URL,它有兩個參數:- 參數一:端點 URL 默認鏈接
- 參數二:替代的 URL 鏈接
-
下面是一些默認的端點 URL:
/oauth/authorize
:授權端點/oauth/token
:令牌端點/oauth/confirm_access
:用戶確認授權提交端點/oauth/error
:授權服務錯誤信息端點/oauth/check_token
:用於資源服務訪問的令牌解析端點
授權端點的 URL 應該被 Spring Security 保護起來只供授權用戶訪問(加入spring security對這些端點進行身份驗證)
可能這些概念現在還不夠太理解,沒關系,先留個印象,完成這個demo之后,再回顧一下就懂了。
建表
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50730
Source Host : localhost:3306
Source Schema : demo
Target Server Type : MySQL
Target Server Version : 50730
File Encoding : 65001
Date: 31/08/2021 17:24:14
*/
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) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_access_token
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userid` varchar(255) DEFAULT NULL,
`clientid` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresat` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`lastmodifiedat` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_approvals
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '客戶端唯一標識',
`client_name` varchar(255) DEFAULT NULL COMMENT '客戶端名稱',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '客戶端所能訪問的資源id集合,多個資源時用逗號(,)分隔',
`client_secret` varchar(255) DEFAULT NULL COMMENT '客戶端密鑰',
`scope` varchar(255) DEFAULT NULL COMMENT '客戶端申請的權限范圍,可選值包括read,write,trust;若有多個權限范圍用逗號(,)分隔',
`authorized_grant_types` varchar(255) NOT NULL COMMENT '客戶端支持的授權許可類型(grant_type),可選值包括authorization_code,password,refresh_token,implicit,client_credentials,若支持多個授權許可類型用逗號(,)分隔',
`web_server_redirect_uri` varchar(255) DEFAULT NULL COMMENT '客戶端重定向URI,當grant_type為authorization_code或implicit時, 在Oauth的流程中會使用並檢查與數據庫內的redirect_uri是否一致',
`authorities` varchar(255) DEFAULT NULL COMMENT '客戶端所擁有的Spring Security的權限值,可選, 若有多個權限值,用逗號(,)分隔',
`access_token_validity` int(11) DEFAULT NULL COMMENT '設定客戶端的access_token的有效時間值(單位:秒),若不設定值則使用默認的有效時間值(60 * 60 * 12, 12小時)',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '設定客戶端的refresh_token的有效時間值(單位:秒),若不設定值則使用默認的有效時間值(60 * 60 * 24 * 30, 30天)',
`additional_information` varchar(4096) DEFAULT NULL COMMENT '這是一個預留的字段,在Oauth的流程中沒有實際的使用,可選,但若設置值,必須是JSON格式的數據',
`autoapprove` varchar(255) DEFAULT 'false' COMMENT '設置用戶是否自動批准授予權限操作, 默認值為 ‘false’, 可選值包括 ‘true’,‘false’, ‘read’,‘write’.',
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='oath2客戶端表';
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('web-client', '前端', NULL, '{bcrypt}$2a$10$jhS817qUHgOR4uQSoEBRxO58.rZ1dBCmCTjG8PeuQAX4eISf.zowm', 'todo.read,todo.write', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost:8081/login/oauth2/code/web-client-auth-code', NULL, 900, 31536000, '{}', NULL);
INSERT INTO `oauth_client_details` VALUES ('resource-server', '資源服務器', NULL, '{bcrypt}$2a$10$jhS817qUHgOR4uQSoEBRxO58.rZ1dBCmCTjG8PeuQAX4eISf.zowm', 'todo.read,todo.write', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost:8081/login/oauth2/code/web-client-auth-code', NULL, 900, 31536000, '{}', NULL);
COMMIT;
-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) NOT NULL,
`token` blob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`token_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_client_token
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='授權碼Code記錄表';
-- ----------------------------
-- Records of oauth_code
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_refresh_token
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for ss_authority
-- ----------------------------
DROP TABLE IF EXISTS `ss_authority`;
CREATE TABLE `ss_authority` (
`id` int(11) NOT NULL COMMENT '主鍵',
`parent_id` int(11) DEFAULT NULL COMMENT '父權限id',
`name` varchar(255) NOT NULL COMMENT '權限名稱',
`desc` varchar(255) DEFAULT NULL COMMENT '權限描述',
`resource` varchar(255) DEFAULT NULL COMMENT '權限資源,當type為1時有值',
`type` int(1) NOT NULL COMMENT '權限類型。0:菜單,1:組件',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='權限表';
-- ----------------------------
-- Records of ss_authority
-- ----------------------------
BEGIN;
INSERT INTO `ss_authority` VALUES (1, NULL, '用戶菜單', '用戶菜單', NULL, 0, '2021-08-23 16:15:15');
INSERT INTO `ss_authority` VALUES (101, 1, '菜單1', '菜單1', NULL, 0, '2021-08-23 16:15:36');
INSERT INTO `ss_authority` VALUES (102, 1, '菜單2', '菜單2', NULL, 0, '2021-08-23 16:15:54');
INSERT INTO `ss_authority` VALUES (10101, 101, '問好', '菜單1功能:問好', 'api:hello', 1, '2021-08-23 16:18:01');
INSERT INTO `ss_authority` VALUES (10201, 102, '用戶名', '菜單2功能:輸出用戶名', 'user:name', 1, '2021-08-23 17:02:08');
COMMIT;
-- ----------------------------
-- Table structure for ss_authority_role_rel
-- ----------------------------
DROP TABLE IF EXISTS `ss_authority_role_rel`;
CREATE TABLE `ss_authority_role_rel` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`authority_id` int(11) NOT NULL COMMENT '權限id',
`role_id` int(11) NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`,`authority_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT='權限—角色關聯表';
-- ----------------------------
-- Records of ss_authority_role_rel
-- ----------------------------
BEGIN;
INSERT INTO `ss_authority_role_rel` VALUES (1, 1, 1);
INSERT INTO `ss_authority_role_rel` VALUES (2, 101, 1);
INSERT INTO `ss_authority_role_rel` VALUES (3, 102, 1);
INSERT INTO `ss_authority_role_rel` VALUES (4, 10101, 1);
INSERT INTO `ss_authority_role_rel` VALUES (5, 10201, 1);
INSERT INTO `ss_authority_role_rel` VALUES (6, 1, 2);
INSERT INTO `ss_authority_role_rel` VALUES (7, 101, 2);
INSERT INTO `ss_authority_role_rel` VALUES (8, 10101, 2);
COMMIT;
-- ----------------------------
-- Table structure for ss_role
-- ----------------------------
DROP TABLE IF EXISTS `ss_role`;
CREATE TABLE `ss_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '角色名',
`desc` varchar(255) DEFAULT NULL COMMENT '角色描述',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色表';
-- ----------------------------
-- Records of ss_role
-- ----------------------------
BEGIN;
INSERT INTO `ss_role` VALUES (1, 'ADMIN', '超級管理員', '2021-08-23 16:08:02');
INSERT INTO `ss_role` VALUES (2, 'USER', '用戶', '2021-08-23 16:08:02');
COMMIT;
-- ----------------------------
-- Table structure for ss_user
-- ----------------------------
DROP TABLE IF EXISTS `ss_user`;
CREATE TABLE `ss_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL COMMENT '用戶名',
`password` varchar(255) NOT NULL COMMENT '密碼',
`status` int(4) DEFAULT NULL COMMENT '狀態',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用戶表';
-- ----------------------------
-- Records of ss_user
-- ----------------------------
BEGIN;
INSERT INTO `ss_user` VALUES (1, 'user', '{bcrypt}$2a$10$jhS817qUHgOR4uQSoEBRxO58.rZ1dBCmCTjG8PeuQAX4eISf.zowm', 1);
INSERT INTO `ss_user` VALUES (2, 'test', '{bcrypt}$2a$10$jhS817qUHgOR4uQSoEBRxO58.rZ1dBCmCTjG8PeuQAX4eISf.zowm', 1);
COMMIT;
-- ----------------------------
-- Table structure for ss_user_role_rel
-- ----------------------------
DROP TABLE IF EXISTS `ss_user_role_rel`;
CREATE TABLE `ss_user_role_rel` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`rid` int(11) NOT NULL COMMENT '角色表id',
`uid` int(11) NOT NULL COMMENT '用戶表id',
PRIMARY KEY (`id`,`rid`,`uid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用戶-角色關聯表';
-- ----------------------------
-- Records of ss_user_role_rel
-- ----------------------------
BEGIN;
INSERT INTO `ss_user_role_rel` VALUES (1, 1, 1);
INSERT INTO `ss_user_role_rel` VALUES (2, 2, 1);
INSERT INTO `ss_user_role_rel` VALUES (3, 2, 2);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
需要關注的是以oauth
開頭的表,其是SpringSecurityOauth2
相關的表,也是官方的表結構
最重要的是oauth_client_details
表,已經有注解了,通過access_token_validity
和refresh_token_validity
去設定token的過期時間
oauth_access_token
和oauth_refresh_token
在token持久化設定為JDBC的形式的時候會使用到,會將生成的access_token
和refresh_token
保存在這兩個表中。如果Token持久化設定為redis或者內存的話,便不需要這兩個表。
接下來我們開始demo的編寫
依賴和配置
新建一個maven項目,引入依賴
<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>authority-server</artifactId>
<groupId>com.cupricnitrate</groupId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<security.oauth2.version>2.5.0.RELEASE</security.oauth2.version>
<security.oauth2.autoconfigure.version>2.3.0.RELEASE</security.oauth2.autoconfigure.version>
<mybatis.plus.version>3.4.3</mybatis.plus.version>
<mysql.version>8.0.25</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Security OAuth2 依賴 -->
<!-- 注意,Spring 已經在開發 Spring Authorization Server,下面三個依賴以后逐漸會棄用 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${security.oauth2.autoconfigure.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${security.oauth2.version}</version>
</dependency>
<!-- 新版 Resource Server 類庫 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- 下面這兩個依賴已經包含在 spring-boot-starter-oauth2-resource-server 中 -->
<!-- JWK 依賴 -->
<!-- <dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.jose.jwt.version}</version>
</dependency> -->
<!-- JWT 依賴 -->
<!-- <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${security.jwt.version}</version>
</dependency> -->
<!--ORM框架-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--token持久化到redis中-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
這個demo采用的依賴版本:
spring-boot
:2.4.3spring-security-oauth2
:2.5.0.RELEASEmybatis-plus
:3.4.3mysql-connector-java
:8.0.25
application.yml
server:
port: 8080
servlet:
context-path: /${spring.application.name}
spring:
application:
#服務名稱
name: authorization-server
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
datasource:
type: com.zaxxer.hikari.HikariDataSource
#mysql驅動8.x版本使用com.mysql.cj.jdbc.Driver
#5.x使用com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
#數據庫地址
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
#數據庫賬號
username: root
#數據庫密碼
password: root
#hikari連接池
hikari:
#2*cpu
maximum-pool-size: 16
#cpu
minimum-idle: 8
data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: true
redis:
port: 6379
host: localhost
database: 0
配置好數據庫,redis
jwk set
授權服務器會提供一個接入點或者api。通過這個api可以直接將服務端的JWK的定義直接拿出來
按照慣例,這個接口是在服務端的.well-known/jwks.json
接口
其返回結構如下:
{
"keys": [
{
# 用什么算法加密
"kty": "RSA",
"e": "AQAB",
# 公鑰
"n": "xxxxx"
}
]
}
客戶端請求token,不是在其內部內嵌一個公鑰,而是從服務器申請,得到公鑰和加密方式
生成密鑰對
之前我們使用的是java工具類去生成一個密鑰對,這里我們使用命令行去生成,並對密鑰對設置存儲密碼
$ keytool -genkeypair -alias 別名 -keyalg RSA -keypass 密鑰的密碼 -keystore 密鑰名 -storepass 存儲密碼
比如:
$ keytool -genkeypair -alias oauth-jwt -keyalg RSA -keypass password -keystore jwt.jks -storepass password
執行之后,會出現一個warning:
Warning:
JKS 密鑰庫使用專用格式。建議使用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 遷移到行業標准格式 PKCS12。
我們按照這個warning進行操作:
$ keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12
輸入存儲密碼,得到密鑰jwt.jks
然后將這個密鑰jwt.jks
放入授權服務的資源文件夾resources
中
對jwk set配置安全表達式
創建JwkSetEndpointConfiguration
類
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration;
/**
* Authorization Server 的安全配置
* 需要配置以下 /.well-known/jwks.json 允許公開訪問
* 在存在多個 Security 配置的情況下,需要設置不同的順序,@Order 是必須的
* 這個配置是授權服務器的,所以優先級高
*
*/
@Order(1)
@Configuration
class JwkSetEndpointConfiguration extends AuthorizationServerSecurityConfiguration {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers(req -> req.mvcMatchers("/.well-known/jwks.json"))
.authorizeRequests(req -> req.mvcMatchers("/.well-known/jwks.json").permitAll());
}
}
寫完代碼后,看見AuthorizationServerSecurityConfiguration
被划了一道橫線,表示這個類已經被棄用了。
瓦特發!你要拿前朝的劍斬當朝的官?
等一下!這似有原因滴!
因為Spring團隊已經在開發 Spring Authorization Server
啦,其旨在代替SpringSecurity中的授權服務器相關依賴。所以SpringSecurity正在慢慢的將授權服務相關的東西丟棄掉。
但是Spring Authorization Server
項目現在正在開發中,並且在現在(2021-09-23)的發布版本是0.2,這個版本有點低,雖然Spring團隊表示這個項目已經脫離實驗階段了,但是在生產環境誰敢用這個0.2?出了碧油雞咋辦?
並且除了AuthorizationServerSecurityConfiguration
這個類,也沒有其他的選擇,所以還是使用這個類。
創建接入點
公鑰轉換成JSON返回
package com.cupricnitrate.authority.oauth2;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
/**
* 為了和 Spring Security 5.1 以上版本的 Resource Server 兼容
* 我們需要讓 Spring Security OAuth2 支持 JWK
* 這個類是為了暴露 JWK 的接入點
* `@FrameworkEndpoint` 注解表示其為框架級的接入點,這里可以理解成一個`@RestController`
*
* @author 硝酸銅
* @date 2021/9/23
*/
@FrameworkEndpoint
class JwkSetEndpoint {
@Resource
private KeyPair keyPair;
@GetMapping("/.well-known/jwks.json")
@ResponseBody
public Map<String, Object> getKey() {
RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
Spring Security 5.1 以上版本的 資源服務在配置了jwt為token之后,其默認配置采用非對稱加密解析jwt簽名,但是配置了授權服務的jwk set地址,便會通過這個url區獲取公鑰,使用對稱加密的方式解析jwt簽名
所以我們這里需要將密鑰對配置上,也就是之前使用命令生成的那個密鑰對
keypair配置
這里我們順便把編碼器也配置上,之后會使用
package com.cupricnitrate.authority.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
/**
* @author 硝酸銅
* @date 2021/9/23
*/
@Configuration
public class WebConfig {
/**
* 編碼器創建
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
//默認編碼算法的Id,新的密碼編碼都會使用這個id對應的編碼器
String idForEncode = "bcrypt";
//要支持的多種編碼器
//舉例:歷史原因,之前用的SHA-1編碼,現在我們希望新的密碼使用bcrypt編碼
//老用戶使用SHA-1這種老的編碼格式,新用戶使用bcrypt這種編碼格式,登錄過程無縫切換
Map encoders = new HashMap();
encoders.put(idForEncode,new BCryptPasswordEncoder());
//encoders.put("SHA-1",new MessageDigestPasswordEncoder("SHA-1"));
//(默認編碼器id,編碼器map)
return new DelegatingPasswordEncoder(idForEncode,encoders);
}
@Bean
public KeyPair keyPair() {
//獲取資源文件中的密鑰
ClassPathResource ksFile = new ClassPathResource("jwt.jks");
//輸入密碼創建KeyStoreKeyFactory
KeyStoreKeyFactory ksFactory = new KeyStoreKeyFactory(ksFile, "password".toCharArray());
//通過別名獲取KeyPair
return ksFactory.getKeyPair("oauth-jwt");
}
}
測試
啟動項目,請求這個接口
這樣一個jwk set接入點就做好了,接下來我們完成授權服務的主要配置
建立授權服務安全配置
授權服務需要使用@EnableAuthorizationServer
來配置授權服務機制,並繼承 AuthorizationServerConfigurerAdapter
該類重寫 configure
方法定義授權服務策略
package com.cupricnitrate.authority.config;
import com.cupricnitrate.authority.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
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.token.TokenEnhancerChain;
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.redis.RedisTokenStore;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.util.Collections;
/**
* `@EnableAuthorizationServer` 注解表示激活授權服務器
* @author 硝酸銅
* @date 2021/8/27
*/
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 密鑰對
*/
@Resource
private KeyPair keyPair;
/**
* 數據源
* RedisTokenStore 需要RedisConnectionFactory,其會讀取配置文件spring.redis的配置
*/
@Resource
private RedisConnectionFactory connectionFactory;
/**
* 數據源
* 讀取配置文件中spring.datasource的配置
*/
@Resource
private DataSource dataSource;
/**
* 用戶service
*/
@Resource
private UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 密碼編碼器
*/
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AuthenticationManager authenticationManager;
/**
* 配置授權服務器的 token 接入點
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 與SpringSecurity中的access()方法類似,設置復雜安全表達式
// 設置可以請求接入點的安全表達式為`permitAll()`
.tokenKeyAccess("permitAll()")
// 設置檢查token的安全表達式為`isAuthenticated()`,已認證
.checkTokenAccess("isAuthenticated()")
// 允許進行表單認證
.allowFormAuthenticationForClients()
// 設置oauth_client_details中的密碼編碼器
.passwordEncoder(passwordEncoder);
}
/**
* 配置 Jdbc 版本的 JdbcClientDetailsService
* 也就是讀取oauth_client_details表的信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 讀取配置文件中spring.datasource的配置
clients.jdbc(dataSource);
}
/**
* 配置授權訪問的接入點
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// endpoints.pathMapping("/oauth/token","/token/login"); 設置token生成請求地址
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(Collections.singletonList(accessTokenConverter()));
endpoints
// chain和accessTokenConverter()二選一即可
//.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(chain)
// token 持久化
.tokenStore(tokenStore())
// 配置認證 manager
.authenticationManager(authenticationManager)
// 配置用戶
.userDetailsService(userDetailsServiceImpl);
}
/**
* 使用jwt生成token
* 如果需要自定義token或者獲取token接口的返回體,需要實現TokenEnhancer接口的enhance方法,具體可以看一下JwtAccessTokenConverter類
* @return JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//非對稱加密簽名
converter.setKeyPair(this.keyPair);
//對稱加密簽名
//converter.setSigningKey("xxx");
return converter;
}
/**
* token持久化
* @return TokenStore
*/
@Bean
public TokenStore tokenStore() {
//將token保存在redis中
return new RedisTokenStore(connectionFactory);
//將token保存在內存中
//return new InMemoryTokenStore();
//不用管如何進行存儲(內存或磁盤),因為它可以把相關信息數據編碼存放在令牌里。JwtTokenStore 不會保存任何數據
//return new JwtTokenStore(accessTokenConverter());
//將token保存在數據庫中
//return new JdbcTokenStore(dataSource);
}
}
具體看代碼,注釋已經寫得很詳細了
其中的userDetailsServiceImpl
所涉及的model和mapper以及其本身的邏輯,和上一節中學習SpringSecurity的邏輯是一樣的,這里就不多做贅述了。
我們來關注一下tokenStore()
這個方法,就是TokenStore
,對token的持久化
token持久化
token持久化主要作用於/oauth/token
(獲取令牌端點接口)和/oauth/check_token
(令牌檢查解析端點接口)。
調用/oauth/token
端點接口時,會先通過TokenStore
判斷持久化容器中是否有這個token,token過期沒有。
如果有這個token並且這個token也沒有過期,則會直接返回這個token,不會生成新的token。如果TokenStore
沒有這個token或者有但是token過期了,則會生成新的token並保存在持久化容器中。
調用/oauth/check_token
端點接口的時候,會先通過TokenStore
判斷持久化容器中是否有這個token,token過期沒有。
如果TokenStore
中有並且沒有過期,則會將token中所包含的荷載信息解析出來。如果TokenStore
沒有這個token或者有但是token過期了,則會報錯token無效。
InMemoryTokenStore
這個是OAuth2默認采用的實現方式。在單服務上可以體現出很好特效(即並發量不大,並且它在失敗的時候不會進行備份),大多項目都可以采用此方法。根據名字就知道了,是存儲在內存中,畢竟存在內存,而不是磁盤中,調試簡易。
JdbcTokenStore
這個是基於JDBC的實現,令牌(Access Token)會保存到數據庫。這個方式,可以在多個服務之間實現令牌共享。
需要有oauth_access_token
表和oauth_refresh_token
表
表的結構在上文ddl中
JwtTokenStore
jwt全稱 JSON Web Token。這個實現方式不用管如何進行存儲(內存或磁盤),因為它可以把相關信息數據編碼存放在令牌里。JwtTokenStore 不會保存任何數據,但是它在轉換令牌值以及授權信息方面與 DefaultTokenServices 所扮演的角色是一樣的。
這種方式有個缺陷,授權服務無法主動控制token的失效,只能等token自己失效過期(登出不能主動控制)
RedisTokenStore
由於TokenStore作用就是對於OAuth2令牌持久化接口,而我們在實際開發中,對於內存的使用是慎之又慎,而對於存儲到數據庫也是根據項目需求進行調配。因此就想,可不可以用redis來進行存儲持久化我們的OAuth2令牌。當然是可以的,並且這也是微服務中保存token使用的較多的方案
AuthenticationManager
AuthenticationManager
來自於授權服務器的WebSecurityConfigurerAdapter
那么現在問題來了:為什么授權服務器還需要WebSecurityConfigurerAdapter
配置呢?不是有AuthorizationServerConfigurerAdapter
就可以了嗎?
AuthorizationServerConfigurerAdapter
只是授權服務的安全配置,來自於依賴spring-security-oauth2
,對oauth2
的授權部分做了配置。但是該依賴中也包含spring-security-core、spring-security-web、spring-security-config
,也就是說其也受SpringSecurity所保護
通過之前的內容我們知道,引入SpringSecurity依賴后,默認會有一個配置,但是這種默認的配置不滿足我們的需求,所以還是需要繼承WebSecurityConfigurerAdapter
做好安全配置
package com.cupricnitrate.authority.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
/**
* @author 硝酸銅
* @date 2021/8/30
*/
@Configuration
@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//授權模式登陸頁面,這里采用默認的登陸頁面
.formLogin()
.and()
.httpBasic()
.and()
.csrf(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
// 授權模式這里采用默認的頁面,登陸成功后從session中拿取回調url
//.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests(req -> req
//oauth2 的接口在 /oauth 接口下面,這里配置其不走SpringSecurity的認證流程
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
) ;
}
@Override
public void configure(WebSecurity web) {
web
.ignoring()
.antMatchers("/error",
"/resources/**",
"/static/**",
"/public/**",
"/h2-console/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**",
"/v2/api-docs/**",
"/doc.html",
"/swagger-resources/**")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
}
學習了之前SpringSecurity的小伙伴看到這個配置,可能會有所疑惑,為什么這里要使用登陸頁面呢?說好的前后端分離呢?
你且聽我慢慢道來~
這個頁面其實都是為了服務Oauth2中的授權碼模式的,授權碼模式一共就需要兩個頁面,一個是登陸頁面,一個是同意授權的頁面。
為了這兩個頁面搞前后端分離,不僅要重寫授權端點接口(獲取授權碼的端點接口源碼就是按照前后端一體的方式做的,其有一個model入參),項目上線的時候還要為這兩個頁面搭一個ng實在是有點不合適,浪費資源!並且我這里只是做一個demo,就使用默認登陸頁面了。
啟動服務,測試端點
到這里一個授權服務就搭好了,我們啟動項目,測試一下
啟動項目前先確定redis正常並且配置正常
密碼模式
調用接口
http://localhost:8080/authorization-server/oauth/token
grant_type
指定為:password
注意,此處需加 Authorization
請求頭,值為 Basic xxx
xxx 為 client_id:client_secret
的 base64編碼。
在https://jwt.io/上面解析token:
授權碼模式
還記得授權碼模式需要訪問頁面嗎?需要登陸和同意授權,這也是為什么授權服務器打開了登陸頁面的原因
瀏覽器訪問:
http://localhost:8080/authorization-server/oauth/authorize?response_type=code&client_id=web-client&client_secret=$2a$10$jhS817qUHgOR4uQSoEBRxO58.rZ1dBCmCTjG8PeuQAX4eISf.zowm&redirect_uri=http://localhost:8081/login/oauth2/code/web-client-auth-code&state=123
參數說明:
redirect_uri
:回調地址,如果數據庫里面配置這個參數,則需要帶上這個參數,並且需要和數據庫中一致state
:回調的時候會帶上這個state
,自定義,可以對其這個字段進行校驗,判斷其是否來自oauth2接口
SpringSecurity發現我們沒有登陸,重定向到登陸頁面
先進行登陸
登陸后跳轉到oauth2默認的授權頁面
點擊Authorize
,進入回調頁面,也就是設置的回調redirect_uri
這里會帶上code
和state
參數,code
就是授權碼,state
就是之前請求時帶上的狀態碼,可用判斷是否是Oauth2
回調返回。
通過code
獲取授權碼,並且code
只能使用一次
注意,此處需加 Authorization
請求頭,值為 Basic xxx
xxx 為 client_id:client_secret
的 base64編碼。
grant_type
值為authorization_code
這里填寫的redirect_uri
也需要和數據庫中的一致