圖解+案例,理解和實戰(zhàn) OAuth2 認證授權
大家好,我是技術UP主小傅哥。
你知道互聯網大廠最怕的是什么嗎?但凡有點這樣的風吹草動,我們就要花費大量的時間進行修復和上線。一點都不敢耽誤,對于緊急類型的,基本當天發(fā)現,當天就要升級上線。那是什么問題呢???
其實最怕的就是各類組件漏洞!
有這么一個東西,13scan - 安全漏洞掃描 它可以掃描出系統(tǒng)組件的各項存在的漏洞,給出整改建議。因為這些漏洞的存在,就可能讓不法用戶通過接口調用到系統(tǒng)數據。比如,隨意輸入個訂單號,就知道是誰,什么時間、購買的什么、地址在哪。這是非??膳碌?。
所以,在互聯網大廠中,會有統(tǒng)一的安全授權認證服務 OAuth2。這樣即使有外部對接的系統(tǒng)確實需要授權獲得用戶的數據,也可以在可靠的范圍內進行授權和使用。
那么,OAuth2 是個啥呢??? 本節(jié)我們來分享下并做個代碼案例運行驗證。
一、OAuth2 是啥?
OAuth 2.0 的標準 RFC 6749,解釋了 OAuth 是什么。
圖片
官網:https://datatracker.ietf.org/doc/html/rfc6749
OAuth 2.0 本身是一種開放標準,不是一個具體的服務類組件,而是一種標準。旨在為用戶提供授權,允許第三方應用程序訪問用戶在某個服務提供者(如社交網絡或云服務)上的信息,而無需將用戶的憑證(如用戶名和密碼)透露給這些應用程序。OAuth 2.0 主要用于授權,而不是身份驗證。
而 Spring 中 OAuth2 就是對這套標準的具體實現,但這不是唯一實現,你甚至可以通過這套標準做一套自己的 OAuth2 授權框架。
二、舉個例子
大家在日常的生活中使用互聯網類的產品,包括;購物、視頻、出行等,都可能收到活動類的短信,問你是否要參與一個這樣的活動,如果參與則需要點擊授權允許。那么這個過程就有 OAuth2 的授權使用。如圖;
圖片
- 這是一套用戶參與小傅哥分享的星球用戶活動頁面。用戶點擊參與后,會引導進入授權驗證。顯示進入微信登錄,之后跳轉到用戶數據授權使用頁。
- 用戶允許授權后,小傅哥的這套活動頁就可以拿到用戶個人的數據,通過個人的數據為唯一標識,允許用戶參與本次活動。這些活動可以是一些抽獎、禮包領取、代碼倉庫授權等。這些場景的使用,就是 OAuth2 的授權框架作用。
三、授權方式
在看 OAuth2 之前,可以代入的思考下,如果是你做一個認證授權框架,你會怎么做。其實你在最開始學習編程使用賬號密碼在數據庫里匹配驗證,完成后生成一個 Token 讓前端保存到 Cookie 里,之后每次請求后端都攜帶上這個 Cookie 進行校驗。
其實這個模型就是認證授權框架。認證;使用賬密證明你是你,授權,則通過賬密分配一個Token,讓使用放通過 Token 進行數據訪問。
那么,OAuth2 作為認證授權框架,提供了四種授權訪問,包括;
- 授權模型(authorization-code)
- 隱藏模式(簡單授權)(implicit)
- 密碼模式(password)
- 客戶端憑證模式(client credentials)
這四種授權方式,逐漸減弱。不過,無論那種授權方式,在第三方應用申請可調用數據的令牌前,都需要先完成系統(tǒng)備案,驗明自身身份。包括客戶端 ID、客戶端秘鑰 Client Secret。
1. 授權模型
授權模式:指第三方應用先申請一個授權碼,之后再使用該碼獲得令牌。授權碼模式通常用于具有瀏覽器界面的應用程序,尤其是在需要用戶交互的場景下,例如傳統(tǒng)的Web應用。由于使用了重定向和授權碼,維護了更高的安全性。
圖片
工作流程:
- 用戶在客戶端(第三方應用)上點擊登錄。
- 客戶端將用戶重定向到授權服務器,攜帶其注冊的客戶端ID、重定向URI和請求的權限范圍。
- 用戶在授權服務器上驗證身份,并同意授權后,授權服務器將用戶重定向回客戶端,附帶一個授權碼。
- 客戶端使用該授權碼向授權服務器請求訪問令牌,同時發(fā)送客戶端ID、客戶端密鑰和重定向URI。
- 授權服務器驗證請求,并返回訪問令牌(和可選的刷新令牌)。
- 客戶端使用訪問令牌訪問受保護的資源。
2. 隱藏模式
隱式模式主要適用于在Web瀏覽器中運行的單頁應用(SPA)等不安全的客戶端環(huán)境,因為不需要后臺服務器交換授權碼,簡化了流程。然而,隱式模式由于直接暴露令牌,安全性較低,不建議用于敏感操作。
圖片
工作流程:
- 用戶在客戶端上點擊登錄。
- 客戶端將用戶重定向到授權服務器,攜帶客戶端ID、重定向URI及請求的權限范圍。
- 用戶在授權服務器進行身份驗證,并同意授權后,授權服務器立即將訪問令牌作為URI片段重定向回客戶端。
- 客戶端在接收到重定向后,解析URI以獲取訪問令牌,隨后可直接使用該令牌訪問受保護的資源。
3. 密碼模式
密碼模式適用于用戶信任客戶端的情況,如用戶通過原生應用(移動應用)訪問服務。在此情況下,客戶端直接處理用戶的憑據,使用時要確保應用的安全性。
圖片
工作流程:
- 用戶在客戶端直接輸入其用戶名和密碼。
- 客戶端將用戶的憑據(用戶名和密碼)發(fā)送到授權服務器,請求訪問令牌。
- 授權服務器驗證憑據并返回訪問令牌(和可選的刷新令牌)。
- 客戶端使用訪問令牌訪問受保護的資源。
4. 客戶端憑證模式
客戶端憑證模式主要用于服務器與服務器之間的通信,如后臺服務相互訪問API,或者服務自身需要訪問其資源。適用于沒有用戶上下文的場景,更多用于機器對機器(M2M)通信。
圖片
工作流程
- 客戶端向授權服務器發(fā)送包含其客戶端ID和客戶端密鑰的請求,請求訪問令牌。
- 授權服務器驗證客戶端身份,并返回訪問令牌。
- 客戶端使用訪問令牌訪問受保護的資源,通常是與服務器本身相關的資源。
四、授權代碼
有了上面的概念,我們再來看個實際的案例工程,驗證四種授權模式。環(huán)境信息如下;
- JDK 1.8
- Maven 3.8.*
- MySQL 5.x ~ 8.x,案例使用的是 8.x
- 工程:https://github.com/fuzhengwei/xfg-dev-tech-oauth2
1. 工程結構
圖片
- 首先,案例工程提供了 OAuth2 的授權框架 + 數據庫配置實現。
- docs 下提供了 docker compose 安裝 MySQL 的腳本和導入庫表的操作,這套庫表就是授權框架的庫表。
- 驗證功能的時候需要使用到 ApiPost,你可以下載使用,并到 api-json 導入。
2. 核心實現
除了 OAuth2 關于 Spring Security 部分已經在前面的課程講解過,可以補充學習。https://bugstack.cn/md/road-map/spring-security.html
2.1 賬戶認證
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String clientId;
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
User clientUser = (User) principal;
clientId = clientUser.getUsername();
} else if (principal instanceof OauthAccountUserDetails) {
getClientIdByRequest();
return (OauthAccountUserDetails) principal;
} else {
throw new UnsupportedOperationException();
}
} else {
clientId = getClientIdByRequest();
}
// 校驗用戶 - 直接從數據庫查詢
OauthAccount account = oauthAccountDao.loadUserByUsername(clientId, username);
if (account == null || !account.getAccountNonDeleted()) {
throw new UsernameNotFoundException("err user is not found!");
}
return new OauthAccountUserDetails(account, new ArrayList<>());
}
2.2 刷新授權
@Bean
public TokenEnhancer additionalInformationTokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> information = new HashMap<>(8);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) userAuthentication;
Object principal = token.getPrincipal();
if (principal instanceof OauthAccountUserDetails) {
OauthAccountUserDetails userDetails = (OauthAccountUserDetails) token.getPrincipal();
OauthAccount oauthAccount = userDetails.getOauthAccount();
information.put("account_info", UserAccountVO.builder()
.id(oauthAccount.getId())
.clientId(oauthAccount.getClientId())
.username(oauthAccount.getUsername())
.mobile(oauthAccount.getMobile())
.email(oauthAccount.getEmail())
.build());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(information);
}
}
return accessToken;
};
}
2.3 添加賬戶
圖片
@Resource
private PasswordEncoder passwordEncoder;
@Test
public void test_passwordEncoder() {
log.info("測試結果:{}", passwordEncoder.encode("123456"));
}
- 這里測試可以生成一個需要的密碼,賬戶填寫到數據庫中使用。
3. 測試驗證
在測試之前,你要啟動服務,確保運行沒問題。啟動前配置數據庫連接。
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.1.109:13306/xfg-dev-tech-oauth2?useUnicode=true&characterEncoding=utf8&autoRecnotallow=true&zeroDateTimeBehavior=convertToNull&serverTimeznotallow=Asia/Shanghai&useSSL=true
driver-class-name: com.mysql.cj.jdbc.Driver
- 關于 ApiPost 的測試,你可以直接從工程中的 json 導入的你的 ApiPost 就可以使用了。
3.1 客戶端憑證
圖片
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYmlnLW1hcmtldC1hcHAiXSwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImV4cCI6MTczNjY3OTA4MCwiYXV0aG9yaXRpZXMiOlsidXNlciJdLCJqdGkiOiI4NWY0YjY2Ni1mNDliLTRiNGEtOTM1Ni0xYjRiMTVmZmI5MWEiLCJjbGllbnRfaWQiOiJ4Zmctc3R1ZGlvIn0.CqMOMbBkHMnQicpkBEeqMyJEp9HbSiGgXoYUke_PWtI",
"token_type": "bearer",
"expires_in": 7198,
"scope": "read write",
"jti": "85f4b666-f49b-4b4a-9356-1b4b15ffb91a"
}
- 請求:http://127.0.0.1:8091/oauth/token?grant_type=client_credentials
- 認證:xfg-studio/123456
3.2 用戶密碼模式
圖片
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYmlnLW1hcmtldC1hcHAiXSwiZXhwIjoxNzM2Njc5MTQxLCJ1c2VyX25hbWUiOiJ4aWFvZnVnZSIsImp0aSI6ImVhZWMzZmQ0LTViOTAtNGRhNy1hODQ1LTA2MDFmMjJiNDc2ZCIsImNsaWVudF9pZCI6InhmZy1zdHVkaW8iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.JgUxx6_aHqCBxuvYXvekw-ZW5pPnSw5LEKlfsd4qVyI",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYmlnLW1hcmtldC1hcHAiXSwidXNlcl9uYW1lIjoieGlhb2Z1Z2UiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXRpIjoiZWFlYzNmZDQtNWI5MC00ZGE3LWE4NDUtMDYwMWYyMmI0NzZkIiwiZXhwIjoxNzM5MjYzOTQyLCJqdGkiOiI5ZDc4ZjVjZS0xZTMwLTRiZTYtYWUyNi01NjY1NWQ4YjYzZjIiLCJjbGllbnRfaWQiOiJ4Zmctc3R1ZGlvIn0.8gMfqhBnc4wI9BsRENu_16RmZFqeCWVSyWcF4B9nA1I",
"expires_in": 7198,
"scope": "read write",
"account_info": {
"id": null,
"clientId": "xfg-studio",
"username": "xiaofuge",
"mobile": "13500002222",
"email": "523088136@qq.com"
}
}
- 請求:http://127.0.0.1:8091/oauth/token
- 認證:xfg-studio/123456
- 參數:grant_type = password、username = xiaofuge、password = 123456
3.3 授權模式
3.3.1 登錄認證
圖片
{
"status": 200,
"message": "hi login success!"
}
- 請求:http://127.0.0.1:8091/login
- 認證:xfg-studio/123456
- 參數:username = xiaofuge、password = 123456
- 說明:你會拿到一個 Cookie JSESSIONID=9000E64733AA6E947054AC4326C91AF8 這個 cookie 用于獲取授權碼
3.3.2 獲取授權碼&跳轉
圖片
圖片
圖片
- 請求:http://127.0.0.1:8091/oauth/authorize?client_id=xfg-studio&response_type=code&grant_type=authorization_code
- 認證:無
- 參數:client_id = xfg-studio、response_type = code、grant_type=authorization_code
- 注意:如果 oauth_client_details 表字段配置 autoapprove = false 則不會直接跳轉頁面,會進行讓用戶確認。
之后刷新令牌、檢查令牌,就可以單獨測試了。如果部署到云服務器,那么還可以走瀏覽器訪問,單獨有一個獲取令牌的操作,之后再跳轉地址。