1.Oauth2
OAuth 2.0授權(quán)框架支持第三方支持訪問有限的HTTP服務(wù),通過在資源所有者和HTTP服務(wù)之間進行一個批準交互來代表資源者去訪問這些資源,或者通過允許第三方應(yīng)用程序以自己的名義獲取訪問權(quán)限。
為了方便理解,可以想象OAuth2.0就是在用戶資源和第三方應(yīng)用之間的一個中間層,它把資源和第三方應(yīng)用隔開,使得第三方應(yīng)用無法直接訪問資源,從而起到保護資源的作用。
為了訪問這種受保護的資源,第三方應(yīng)用(客戶端)在訪問的時候需要提供憑證。即,需要告訴OAuth2.0你是誰你要做什么。
用戶可以將用戶名和密碼告訴第三方應(yīng)用,讓第三方應(yīng)用直接以你的名義去訪問,也可以授權(quán)第三方應(yīng)用去訪問。
例如,微信公眾平臺開發(fā),在微信公眾平臺開發(fā)過程中當我們訪問某個頁面,頁面可能彈出一個提示框應(yīng)用需要獲取我們的個人信息問是否允許,點確認其實就是授權(quán)第三方應(yīng)用獲取我們在微信公眾平臺的個人信息,這里微信網(wǎng)頁授權(quán)就是使用的OAuth2.0。
-
第三方應(yīng)用程序(Third-party application): 又稱之為客戶端(client),我們自己開發(fā)的各種客戶端,對我們自己的項目來說,QQ、微信、支付寶等是第三方應(yīng)用程序。
-
HTTP 服務(wù)提供商(HTTP service): 我們開發(fā)的項目以及 QQ、微信、支付寶、釘釘?shù)榷伎梢苑Q之為“服務(wù)提供商”。
-
資源所有者(Resource Owner): 又稱之為用戶(user),擁有賬號密碼的人。
-
用戶代理(User Agent): 用來訪問資源,比如瀏覽器,代替用戶去訪問這些資源。
-
認證服務(wù)器(Authorization server): 即服務(wù)提供商專門用來處理認證的服務(wù)器,主要就是實現(xiàn)登錄、授權(quán)功能。
-
資源服務(wù)器(Resource server): 即服務(wù)提供商存放用戶生成的資源的服務(wù)器,比如電商中的商品模塊、訂單模塊等,是用來處理具體業(yè)務(wù)的服務(wù)器。
????????OAuth2.0協(xié)議流程描述了四種角色之間的交互過程,如下圖所示。
????????簡單說,OAuth 就是一種授權(quán)機制。數(shù)據(jù)的所有者告訴系統(tǒng),同意授權(quán)第三方應(yīng)用進入系統(tǒng),獲取這些數(shù)據(jù)。系統(tǒng)從而產(chǎn)生一個短期的進入令牌(token),用來代替密碼,供第三方應(yīng)用使用。
令牌(token)與密碼(password)的作用是一樣的,都可以進入系統(tǒng),但是有三點差異。
-
令牌是短期的,到期會自動失效,用戶自己無法修改。密碼一般長期有效,用戶不修改,就不會發(fā)生變化。
-
令牌可以被數(shù)據(jù)所有者撤銷,會立即失效。
-
令牌有權(quán)限范圍(scope),對于網(wǎng)絡(luò)服務(wù)來說,只讀令牌就比讀寫令牌更安全。密碼一般是完整權(quán)限。
上面這些設(shè)計,保證了令牌既可以讓第三方應(yīng)用獲得權(quán)限,同時又隨時可控,不會危及系統(tǒng)安全。這就是 OAuth 2.0 的優(yōu)點。
注意,只要知道了令牌,就能進入系統(tǒng)。系統(tǒng)一般不會再次確認身份,所以令牌必須保密,泄漏令牌與泄漏密碼的后果是一樣的。 這也是為什么令牌的有效期,一般都設(shè)置得很短的原因。
1.1 開放平臺
????????開放平臺(Open Platform)在軟件行業(yè)和網(wǎng)絡(luò)中,開放平臺是指軟件系統(tǒng)通過公開其應(yīng)用程序編程接口(API)或函數(shù)(function)來使外部的程序可以增加該軟件系統(tǒng)的功能或使用該軟件系統(tǒng)的資源,而不需要更改該軟件系統(tǒng)的源代碼。
????????在互聯(lián)網(wǎng)時代,把網(wǎng)站的服務(wù)封裝成一系列計算機易識別的數(shù)據(jù)接口開放出去,供第三方開發(fā)者使用,這種行為就叫做Open API,提供開放API的平臺本身就被稱為開放平臺。
????????第一種是技術(shù)性的開放,例如百度、騰訊、阿里巴巴等,例如阿里可以提供標準化的應(yīng)用軟件,但是數(shù)百萬形形色色的賣家對于個性化要求的軟件,并不是一個公司的力量可以滿足的,所以就把這些需求開放給眾多的第三方開發(fā)者的方式。再例如google的基于Linux平臺的開源手機操作系統(tǒng)就被認為會很快打敗Nokia塞班系統(tǒng)。這一種技術(shù)性開放平臺雖然目前來看跟B2C企業(yè)的開放平臺關(guān)系不大,但是也能從一定程度上說明開放平臺是互聯(lián)網(wǎng)企業(yè)的趨勢。
????????第二種開放平臺是指軟件系統(tǒng)通過公開其應(yīng)用程序編程接口(API)或函數(shù)(function)來使外部的程序可以增加該軟件系統(tǒng)的功能或使用該軟件系統(tǒng)的資源,而不需要更改該軟件系統(tǒng)的源代碼。B2C企業(yè)開放平臺又包含兩種形式,A:淘寶商城、日本樂天這種純平臺的模式,即自己不碰商品的進銷存,全部由入駐商家來做;B:美國亞馬遜、當當網(wǎng)、京東商城這種“自營+聯(lián)營”的模式。
1.2 開放平臺交互模型
三個角色:
-
資源擁有者:用戶
-
客戶端:各種app、瀏覽器
-
服務(wù)提供方:包含兩個角色
認證服務(wù)器
資源服務(wù)器
1.2.1 認證服務(wù)器
認證服務(wù)器負責(zé)對用戶進行認證,并授權(quán)給客戶端權(quán)限。一般的認證都是通過對賬號密碼進行驗證實現(xiàn),而難點在于怎么進行授權(quán)。比如我們使用第三方登錄 "嗶哩嗶哩",可以看到如使用 QQ 登錄的授權(quán)頁面上有 "嗶哩嗶哩將獲取以下權(quán)限" 的字樣以及權(quán)限信息
認證服務(wù)器需要知道請求授權(quán)的客戶端的身份以及該客戶端請求的權(quán)限。常見的做法是為每一個客戶端預(yù)先分配一個 id,并給每個 id 對應(yīng)一個名稱以及權(quán)限信息。這些信息可以寫在認證服務(wù)器上的配置文件里,今后客戶端每次打開授權(quán)頁面的時候,客戶端需要將該id發(fā)送到認證服務(wù)器,0Auth2.0就可以用來自動給客戶端分配id,同時完成配置文件的自動更新。
1.3 OAuth2 開放平臺
開放平臺是由 OAuth2.0 協(xié)議發(fā)展而來的一個產(chǎn)品,它的作用是讓客戶端自己去這上面進行注冊、申請,通過之后系統(tǒng)自動分配 客戶端id ,并完成配置的自動更新。
客戶端要完成申請,通常需要申請人填寫客戶端程序的類型(Web、App、微信小程序、支付寶小程序等等)、企業(yè)信息、營業(yè)執(zhí)照、法人信息以及想要獲取權(quán)限等信息,申請需要得到得到服務(wù)提供上的審核通過之后,開發(fā)平臺才會自動分配一個客戶端id給客戶端。
在通過審核之后,第三方應(yīng)用在進行認證時,就會想需要獲取到的權(quán)限信息展示到頁面上,例如嗶哩嗶哩獲取QQ權(quán)限。授權(quán)成功之后認證服務(wù)器需要把產(chǎn)生的 access_token 發(fā)送給客戶端,客戶端才能訪問具體的資源(頭像、性別之類的),大致過程如下:
-
讓客戶端在開放平臺提交申請時候,填寫一個 網(wǎng)址,例如:www.baidu.com,此網(wǎng)址主要用來獲取認證碼。
-
當有用戶授權(quán)成功之后,認證服務(wù)器將頁面重定向到這個網(wǎng)址,并將生成的 access_token拼接到該網(wǎng)址后面,例如:www.baidu.com?access_token=123?
-
客戶端接收到access_token,之后客戶端就可以拿著這個token去獲取需要的數(shù)據(jù)了
1.3.1 令牌
傳統(tǒng)項目向服務(wù)端請求數(shù)據(jù),服務(wù)端需要頻繁的去數(shù)據(jù)庫查詢用戶名和密碼并進行對比,判斷用戶名和密碼正確與否,并作出相應(yīng)提示,這樣效率非常低下,怎么提高效率呢?Token便應(yīng)運而生。
Token是服務(wù)端生成的一串字符串,以作客戶端進行請求的一個令牌,當?shù)谝淮蔚卿浐螅?wù)器生成一個Token便將此Token返回給客戶端,以后客戶端只需帶上這個Token前來請求數(shù)據(jù)即可,無需再次帶上用戶名和密碼。減輕服務(wù)器的壓力,減少頻繁的查詢數(shù)據(jù)庫,使服務(wù)器更加健壯。
1.3.2 Access Token
Access Token 是客戶端訪問資源服務(wù)器的令牌。擁有這個令牌代表著得到用戶的授權(quán),即具備了訪問資源的權(quán)限。同時這個授權(quán)應(yīng)該是臨時的,只能在一定期限內(nèi)使用。主要原因是因為Access Token 在使用的過程中很有可能會泄露,被不法分子利用獲取我們的數(shù)據(jù)。所以Access Token應(yīng)該只能在某個期限內(nèi)使用,這樣可以降低因 Access Token 泄露而帶來的風(fēng)險。
1.4?認證模式
OAuth2.0中定義了四種授權(quán)模式:
-
authorization code 授權(quán)碼模式
-
implicit 簡化模式
-
resource owner password credentials 密碼模式
-
client credentials 客戶端模式
常見模式:授權(quán)碼、密碼模式
1.4.1 授權(quán)碼模式
授權(quán)碼模式(authorization code)是功能最完整、流程最嚴密的授權(quán)模式,code保證了token的安全性,即使code被攔截,由于沒有secret,也是無法通過code獲得token的。
角色行為與功能
-
資源所有者
只需要允許或拒絕第三方應(yīng)用獲得授權(quán)
-
第三方應(yīng)用
申請成為資源服務(wù)器的第三方應(yīng)用
獲取資源服務(wù)器提供的資源
-
授權(quán)服務(wù)器
提供授權(quán)許可code、令牌token等
-
資源服務(wù)器
提供給第三方應(yīng)用開放資源的接口
時序圖
環(huán)境搭建
創(chuàng)建父項目
指定打包方式為pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.woniuxy</groupId>
<artifactId>oauth2</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
</project>
創(chuàng)建auth-server認證服務(wù)器模塊
導(dǎo)入依賴
導(dǎo)入依賴版本如下
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</properties>
oauth2依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
創(chuàng)建用戶信息配置類
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{
//密碼編碼器
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 基于內(nèi)存的用戶信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication() //內(nèi)存認證
.withUser("zhangsan") //用戶名
.password(passwordEncoder().encode("123")) //密碼
.authorities("ROLE_ADMIN"); //角色
}
}
創(chuàng)建客戶端配置類,配置客戶端信息
import org.springframework.context.annotation.Configuration;
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 javax.annotation.Resource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{
@Resource
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客戶端
clients
.inMemory() //內(nèi)存方式
.withClient("client") //客戶端名字
.secret(passwordEncoder.encode("secret")) //客戶端秘鑰
.authorizedGrantTypes("authorization_code")//授權(quán)類型
.scopes("all") //授權(quán)范圍
.redirectUris("http://www.baidu.com"); //回調(diào)網(wǎng)址,攜帶授權(quán)碼
}
}
在application.yml文件中配置以下信息
server:
port: 8000
spring:
application:
name: oauth
啟動項目進行登錄
localhost:8000/login
進入登錄頁面,輸入賬號:zhangsan,密碼:123進行登錄
登錄成功之后向服務(wù)器發(fā)送請求獲取授權(quán)碼,在地址欄上輸入以下內(nèi)容回車
http://localhost:8080/oauth/authorize?client_id=client&response_type=code
可以看到一個授權(quán)頁面,詢問用戶是否進行授權(quán)
授權(quán)成功之后會重定向到AuthorizationServerConfiguration配置類中指定的地址,并以參數(shù)的方式攜帶授權(quán)碼
通過postman發(fā)送請求向服務(wù)器獲取token
地址欄填寫:http://client:secret@localhost:8000/oauth/token
填寫客戶端賬號密碼
填寫授權(quán)類型、授權(quán)碼,發(fā)送請求
成功之后在postman上可以看到以下信息
表示成功
注意:每個授權(quán)碼只能使用一次
1.4.2 密碼模式
密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼??蛻舳耸褂眠@些信息,向"服務(wù)商提供商"索要授權(quán)。
在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統(tǒng)的一部分,或者由一個著名公司出品。而認證服務(wù)器只有在其他授權(quán)模式無法執(zhí)行的情況下,才能考慮使用這種模式。
修改AuthorizationServerConfiguration配置類,添加密碼模式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客戶端
clients
.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("authorization_code","password") //添加密碼授權(quán)模式
.scopes("all") //授權(quán)范圍
.redirectUris("http://www.woniuxy.com");
}
在postman中新開一個請求,地址欄中填寫:http://localhost:8080/oauth/token
密碼授權(quán)模式要求以請求頭的方式提交客戶端賬號密碼,并且需要對賬號密碼進行base64加密,因此選擇Authorization選項卡,設(shè)置TYPE為"Basic Auth",并填寫客戶端賬號密碼
在請求體中設(shè)置授權(quán)類型、用戶賬號密碼參數(shù)
發(fā)送請求測試
可以發(fā)現(xiàn)此時并不支持密碼模式,即使在AuthorizationServerConfiguration配置類中指定了密碼模式。
原因是此時代碼中缺少對密碼模式的支持,在oauth2中需要添加AuthenticationManager對象對密碼模式進行支持。
在WebSecurityConfiguration配置類中配置 AuthenticationManager
// 配置 AuthenticationManager(密碼模式需要該對象進行賬號密碼校驗)
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
在AuthorizationServerConfiguration類中注入AuthenticationManager,并重寫以下方法
// 認證管理器
@Autowired
private AuthenticationManager authenticationManager;
//配置使用的 AuthenticationManager 實現(xiàn)用戶認證的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
重啟項目再次發(fā)送請求獲取token
整合JWT
導(dǎo)入了oauth2依賴就自動導(dǎo)入的JWT相關(guān)依賴,因此不用單獨導(dǎo)入JWT,只需要進行設(shè)置就行
創(chuàng)建TokenConfiguration配置類
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
public class TokenConfiguration {
// 密碼
private static String SIGNING_KEY="www.woniuxy.com";
// token轉(zhuǎn)換器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter =
new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
// 令牌存儲策略:jwt方式
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConverter());
}
}
在AuthorizationServerConfiguration配置類中注入相關(guān)對象
@Resource
private TokenStore tokenStore;
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Resource
private ClientDetailsService clientDetailsService;
在AuthorizationServerConfiguration配置類中編寫token服務(wù)方法,該方法主要用來設(shè)置
private AuthorizationServerTokenServices tokenServices(){
// 創(chuàng)建服務(wù)對象
DefaultTokenServices services = new DefaultTokenServices();
// 設(shè)置客戶端詳情服務(wù)
services.setClientDetailsService(clientDetailsService);
// 支持刷新令牌
services.setSupportRefreshToken(true);
// 不重復(fù)使用refreshtoken,每次刷新之后只能用新的refreshtoken才能繼續(xù)刷新
services.setReuseRefreshToken(false);
// 設(shè)置令牌存儲策略
services.setTokenStore(tokenStore);
// 設(shè)置令牌增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
// 設(shè)置令牌過期時間
services.setAccessTokenValiditySeconds(600);
services.setRefreshTokenValiditySeconds(6000);
return services;
}
修改configure(AuthorizationServerEndpointsConfigurer endpoints)方法,添加token服務(wù)
//配置使用的 AuthenticationManager 實現(xiàn)用戶認證的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) // 認證管理器
.tokenServices(tokenServices()); // 配置token服務(wù)
}
重啟項目發(fā)送請求獲取token
如果想要獲取到refreshtoken,可以修改AuthorizationServerConfiguration配置類,添加refresh_token授權(quán)方式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客戶端
clients
.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("authorization_code","password","refresh_token")
.scopes("all")
.redirectUris("http://www.woniuxy.com");
}
重啟項目測試
整合數(shù)據(jù)庫(user)
建表SQL
create database sc default character set=utf8;
DROP TABLE IF EXISTS `perms`;
CREATE TABLE `perms` (
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `perms` VALUES (3001,'user:add'),(3002,'user:del'),(3003,'user:find'),(3004,'user:update'),(3005,'goods:add'),(3006,'goods:find'),(3007,'goods:del'),(3008,'goods:update');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `role` VALUES (2001,'ROLE_ADMIN'),(2002,'ROLE_USER');
DROP TABLE IF EXISTS `role_perms`;
CREATE TABLE `role_perms` (
`rid` int(11) DEFAULT NULL,
`pid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `role_perms` VALUES (2001,3001),(2001,3003),(2001,3004),(2002,3005),(2002,3006),(2002,3007),(2002,3008);
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) DEFAULT NULL,
`username` varchar(20) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES (1001,'zhangsan','$2a$10$pINVnd8.cXScFXCxI2x4cem4fOexA2J5TNY/Mx2CjN6mJuYGBNG0m'),(1002,'wangwu','wangwu');
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user_role` VALUES (1001,2001),(1002,2002),(1003,2002);
auth-server的pom.xml中引入mybatis
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
在application.yml中配置mybatis參數(shù)
mybatis:
type-aliases-package: com.woniuxy.authserver.entity
mapper-locations: classpath:/mapper/*.xml
創(chuàng)建Perms、Role、User實體類,注意:實體類必須實現(xiàn)序列化接口,不然運行過程中可能會報Failed to find access token for token錯誤
import lombok.Data;
@Data
public class Perms implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
}
import lombok.Data;
import java.util.List;
@Data
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private List<Perms> perms;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String username;
private String password;
private List<Role> roles;
// 返回當前用戶的所有角色、權(quán)限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
log.debug("獲取用戶角色權(quán)限信息");
// 新建集合
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// 遍歷role
for(Role role : this.roles){
// 放入角色信息
grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
// 遍歷當前角色的所有權(quán)限信息
for(Perms perms : role.getPerms()){
grantedAuthorities.add(new SimpleGrantedAuthority(perms.getName()));
}
}
log.debug(grantedAuthorities.toString());
return grantedAuthorities;
}
// 獲取用戶名
@Override
public String getUsername() {
return this.username;
}
// 賬號是否過期 true表示未過期 false表示過期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 賬號是否被鎖定 true表示未鎖定 false表示鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 憑證是否過期 true表示未過期 false表示過期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 用戶是否被禁用 true表示未禁用 false表示禁用
@Override
public boolean isEnabled() {
return true;
}
}
創(chuàng)建UerMapper接口
import com.woniuxy.springsecurity.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
public User findByName(String username);
}
在resources目錄下創(chuàng)建mapper文件夾,并在該文件夾下創(chuàng)建Mapper文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.woniuxy.authserver.mapper.UserMapper" >
<select id="findByName" resultMap="user_map">
select * from user where username = #{username}
</select>
<resultMap id="user_map" type="User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<collection property="roles" ofType="Role" column="id" select="findRolesByUid"></collection>
</resultMap>
<select id="findRolesByUid" resultMap="role_map">
select r.id,r.name from user_role ur,role r where ur.rid = r.id and ur.uid = #{id}
</select>
<resultMap id="role_map" type="Role">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<collection property="perms" ofType="Perms" column="id" select="findPermsByRid"></collection>
</resultMap>
<select id="findPermsByRid" resultType="Perms">
select p.id,p.name from role_perms rp,perms p where rp.pid = p.id and rp.rid = #{rid}
</select>
</mapper>
創(chuàng)建CustomUserDetailsServiceImpl類實現(xiàn)UserDetailsService接口
import com.woniuxy.authserver.entity.User;
import com.woniuxy.authserver.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.查詢用戶
User user = userMapper.findByName(username);
//2.判斷
if (user == null) throw new UsernameNotFoundException("用戶不存在");
//3.返回用戶信息
return user;
}
}
在配置類WebSecurityConfiguration中注入UserDetailsService對象,并修改configure(AuthenticationManagerBuilder auth)反方指定用戶信息從數(shù)據(jù)庫中獲取
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth
//.inMemoryAuthentication() //內(nèi)存認證
//.withUser("zhangsan") //用戶名
//.password(passwordEncoder().encode("123")) //密碼
//.authorities("ROLE_ADMIN"); //角色
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
重啟auth-server服務(wù),進行認證
封裝用戶id
在生成token時可以將用戶id封裝到token中,以便后期使用
修改TokenConfiguration類中的accessTokenConverter()方法,在創(chuàng)建轉(zhuǎn)換器時重寫enhance方法
// token轉(zhuǎn)換器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter =
new JwtAccessTokenConverter(){
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String,Object> map = new HashMap<>();
// 從認證對象中得到用戶信息
User user = (User) authentication.getUserAuthentication().getPrincipal();
// 將用戶id放到token中
map.put("uid", user.getId());
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);
// 返回
return super.enhance(accessToken, authentication);
}
};
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
利用postman進行測試
返回的結(jié)果中可以看到用戶id,token中也包含了用戶id
檢驗token是否過期
在org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint類中定義了校驗token的接口/oauth/check_token,該接口可以用來校驗token是否合法、是否過期、是否是偽造的
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
// gh-1070
response.put("active", true); // Always true if token exists and not expired
return response;
}
只是該接口oauth2默認情況下是不對外公開的,如果要使用該接口那就必須手動配置開啟,在AuthorizationServerConfiguration配置類中重寫以下方法
//設(shè)置 /oauth/check_token 端點,通過認證后可訪問。
//該端點對應(yīng) CheckTokenEndpoint類,用于校驗訪問令牌的有效性。
//在客戶端訪問資源服務(wù)器時,會在請求中帶上訪問令牌。
//在資源服務(wù)器收到客戶端的請求時,會使用請求中的訪問令牌,找授權(quán)服務(wù)器確認該訪問令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 默認是denyAll():拒絕所有
oauthServer.checkTokenAccess("permitAll()");
}
checkTokenAccess常用值有三種:
-
denyAll():拒絕所有請求,不開放該接口
-
isAuthenticated():只對完成認證之后的請求開放
-
permitAll():對所有請求開放
測試:登錄成功之后在Postman中發(fā)送請求進行測試
接口url:http://localhost:8080/oauth/check_token
返回的結(jié)果中包含了用戶的用戶名、權(quán)限等信息,還包括了token是否可用的信息
如果返回以下信息表示token已經(jīng)過期
而如果返回以下信息表示token非法
通過refresh_token獲取新token
獲取token和刷新token使用的是同一個接口,所以地址欄url還是
http://local:8080/oauth/token
只是grant_type需要換成refresh_token,然后將之前的refresh token作為參數(shù)傳遞給后臺
還是需要將客戶端id、密碼以base64編碼放到請求頭中
發(fā)送請求得到結(jié)果
根據(jù)結(jié)果可以知道,token和refresh_token都會自動刷新,這樣做的好處是當token過期時通過程序調(diào)用刷新接口,獲取到新的token和refresh_token,實現(xiàn)自動續(xù)期。
refresh_token如果過期會得到以下結(jié)果
refresh_token過期就需要重新登錄
1.5 資源服務(wù)器
創(chuàng)建resource子模塊,導(dǎo)入相關(guān)依賴
設(shè)置父子關(guān)系
創(chuàng)建OAuth2ResourceServerConfig配置類
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 設(shè)置請求,需要認證后訪問
.anyRequest().authenticated();
}
}
創(chuàng)建controller
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/resource")
public class ResourceController {
@RequestMapping("/info")
public String info(){
return "success";
}
}
配置application.yml
server:
port: 8001
spring:
application:
name: resource
security:
oauth2:
# OAuth2 Client 配置,對應(yīng) OAuth2ClientProperties 類
client:
client-id: client
client-secret: secret
# OAuth2 Resource 配置,對應(yīng) ResourceServerProperties 類
resource:
token-info-uri: http://127.0.0.1:8000/oauth/check_token # 獲得 Token 信息的 URL
# 訪問令牌獲取 URL,自定義的
access-token-uri: http://127.0.0.1:8000/oauth/token
management:
endpoints:
web:
exposure:
include: '*'
啟動resource資源服務(wù)器
先進行認證,得到token和refresh_token
localhost:8000/oauth/token
然后將得到的token放到請求資源服務(wù)器的請求頭中
發(fā)送請求后可以發(fā)現(xiàn)報500錯誤,查看resource控制臺可以發(fā)現(xiàn)以下信息
org.springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2022-05-07T03:40:14.063+00:00","status":403,"error":"Forbidden","message":"","path":"/oauth/check_token"}]
根據(jù)信息提示:沒有權(quán)限訪問 /oauth/check_token,該URL是認證服務(wù)器用來校驗token是否合法的接口。資源服務(wù)器在接收到請求時會獲取到token,然后調(diào)用認證服務(wù)器的/oauth/check_token接口去檢驗token,但是此時認證服務(wù)器還沒有開放該端口(默認關(guān)閉),所以造成了403無法訪問。
到認證服務(wù)器的AuthorizationServerConfiguration配置類中開啟/oauth/check_token
//設(shè)置 /oauth/check_token 端點,通過認證后可訪問。
//該端點對應(yīng) CheckTokenEndpoint類,用于校驗訪問令牌的有效性。
//在客戶端訪問資源服務(wù)器時,會在請求中帶上訪問令牌。
//在資源服務(wù)器收到客戶端的請求時,會使用請求中的訪問令牌,找授權(quán)服務(wù)器確認該訪問令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 默認是denyAll():拒絕所有
oauthServer.checkTokenAccess("isAuthenticated()");
}
重啟認證服務(wù)器
重新進行認證得到token,然后用新的token再訪問資源服務(wù)器
看到success表明成功
角色權(quán)限管理
在資源服務(wù)器主啟動類上添加@EnableGlobalMethodSecurity注解,開啟spring security權(quán)限注解的支持
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
在resource/info接口方法上添加注解@PreAuthorize并指定角色或權(quán)限
@RequestMapping("/info")
@PreAuthorize("hasRole('USER')")
public String info(){
return "success";
}
利用postman再次訪問該接口
得到不允許訪問的結(jié)果,表明角色權(quán)限管理生效
1.6 整合數(shù)據(jù)庫(client)
建表SQL
CREATE TABLE `clientdetails` (
`appId` VARCHAR(128) NOT NULL,
`resourceIds` VARCHAR(256) DEFAULT NULL,
`appSecret` VARCHAR(256) DEFAULT NULL,
`scope` VARCHAR(256) DEFAULT NULL,
`grantTypes` VARCHAR(256) DEFAULT NULL,
`redirectUrl` VARCHAR(256) DEFAULT NULL,
`authorities` VARCHAR(256) DEFAULT NULL,
`access_token_validity` INT(11) DEFAULT NULL,
`refresh_token_validity` INT(11) DEFAULT NULL,
`additionalInformation` VARCHAR(4096) DEFAULT NULL,
`autoApproveScopes` VARCHAR(256) DEFAULT NULL,
PRIMARY KEY (`appId`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_access_token` (
`token_id` VARCHAR(256) DEFAULT NULL,
`token` BLOB,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) DEFAULT NULL,
`client_id` VARCHAR(256) DEFAULT NULL,
`authentication` BLOB,
`refresh_token` VARCHAR(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_approvals` (
`userId` VARCHAR(256) DEFAULT NULL,
`clientId` VARCHAR(256) DEFAULT NULL,
`scope` VARCHAR(256) DEFAULT NULL,
`status` VARCHAR(10) DEFAULT NULL,
`expiresAt` TIMESTAMP NULL DEFAULT NULL,
`lastModifiedAt` TIMESTAMP NULL DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_details` (
`client_id` VARCHAR(128) NOT NULL,
`resource_ids` VARCHAR(256) DEFAULT NULL,
`client_secret` VARCHAR(256) DEFAULT NULL,
`scope` VARCHAR(256) DEFAULT NULL,
`authorized_grant_types` VARCHAR(256) DEFAULT NULL,
`web_server_redirect_uri` VARCHAR(256) DEFAULT NULL,
`authorities` VARCHAR(256) DEFAULT NULL,
`access_token_validity` INT(11) DEFAULT NULL,
`refresh_token_validity` INT(11) DEFAULT NULL,
`additional_information` VARCHAR(4096) DEFAULT NULL,
`autoapprove` VARCHAR(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_token` (
`token_id` VARCHAR(256) DEFAULT NULL,
`token` BLOB,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) DEFAULT NULL,
`client_id` VARCHAR(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_code` (
`code` VARCHAR(256) DEFAULT NULL,
`authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
`token_id` VARCHAR(256) DEFAULT NULL,
`token` BLOB,
`authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;
在表 oauth_client_details 中增加一條客戶端配置記錄,在填入時可以按照AuthorizationServerConfiguration配置類中的客戶端配置進行配置
配置的效果如下:
注:各字段解釋說明
-
client_id:客戶端標識
-
client_secret:客戶端安全碼。注意安全碼不能是明文需要加密,此處可以寫一段程序,然后使用BCryptPasswordEncoder為客戶端安全碼加密,得到加密之后的安全碼,再寫入到數(shù)據(jù)庫中,例如:
-
System.out.println(new BCryptPasswordEncoder().encode("secret"));
-
scope:客戶端授權(quán)范圍
-
authorized_grant_types:客戶端授權(quán)類型,支持多種類型,多種類型之間用逗號隔開
-
web_server_redirect_uri:服務(wù)器回調(diào)地址
創(chuàng)建實體類User、Role、Perms
在auth-server模塊的pom.xml中引入mybatis相關(guān)依賴
<!-- spring-boot-starter-jdbc 內(nèi)置了HikariCP 連接池,所以使用該連接池連接數(shù)據(jù)庫 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 獲取application.yml文件中的配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
application.yml文件中添加數(shù)據(jù)庫相關(guān)配置
server:
port: 8000
spring:
application:
name: oauth
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/sc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
hikari:
minimum-idle: 5
maximum-pool-size: 10
auto-commit: true #自動提交
pool-name: MYHIKARICP
connection-test-query: SELECT 1 #測試是否能連接上數(shù)據(jù)庫的SQL語句
main:
#true,后定義的bean會覆蓋之前定義的相同名稱的bean,生成dataSource替換掉原生的dataSource
allow-bean-definition-overriding: true
創(chuàng)建數(shù)據(jù)庫配置類DataSourceConfiguration,主要配置用到的數(shù)據(jù)源,用HikariCP連接池的數(shù)據(jù)源替換到spring內(nèi)置的數(shù)據(jù)源。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfiguration {
@Bean
@Primary
//根據(jù)application.yml中的配置信息創(chuàng)建dataSource
@ConfigurationProperties(prefix = "spring.datasource")
//import javax.sql.DataSource;
public DataSource dataSource() {
//創(chuàng)建dataSource
return DataSourceBuilder.create().build();
}
}
在TokenConfiguration配置類中把token存儲策略改成JDBC方式,將jwt存放到數(shù)據(jù)庫中DataSource
@Resource
private DataSource dataSource;
// 令牌存儲策略:jwt方式
@Bean
public TokenStore tokenStore(DataSource dataSource){
//return new JwtTokenStore(accessTokenConverter());
return new JdbcTokenStore(dataSource);
}
修改AuthorizationServerConfiguration配置類,添加ClientDetailsService clientDetailsService(DataSource dataSource)方法,讓程序通過DataSource從數(shù)據(jù)庫中獲取到客戶端信息
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
//在數(shù)據(jù)庫中去獲取客戶端信息(oauth_client_details表)
return new JdbcClientDetailsService(dataSource);
}
修改configure(ClientDetailsServiceConfigurer clients)方法指定到數(shù)據(jù)庫獲取客戶端信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客戶端
//clients
//.inMemory() //內(nèi)存方式
//.withClient("client") //客戶端名字
//.secret(passwordEncoder.encode("secret")) //客戶端秘鑰
//.authorizedGrantTypes("authorization_code","password","refresh_token")
//.scopes("all") //授權(quán)范圍
//.redirectUris("http://www.woniuxy.com"); //回調(diào)網(wǎng)址
clients.withClientDetails(clientDetailsService);
}
完成之后重啟項目,再次進行認證測試
正常情況下,測試完畢之后會在數(shù)據(jù)庫的oauth_access_token 表中會增加一個記錄,這個記錄就是瀏覽器獲取到的token和refresh token
角色、權(quán)限管理測試
在resource服務(wù)的controller中添加以下方法文章來源:http://www.zghlxwxcb.cn/news/detail-687298.html
@RequestMapping("/message")
@PreAuthorize("hasRole('ADMIN')")
public String message(){
return "message";
}
@RequestMapping("/data")
@PreAuthorize("hasAuthority('user:add')")
public String data(){
return "data";
}
@RequestMapping("/test")
@PreAuthorize("hasAuthority('user:del')")
public String test(){
return "test";
}
啟動resource服務(wù),通過postman分別測試info、message、data、test接口,如果只有message、data接口可以訪問,那么說明角色、權(quán)限管理成功。文章來源地址http://www.zghlxwxcb.cn/news/detail-687298.html
到了這里,關(guān)于18.Oauth2-微服務(wù)認證的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!