javascript
微服务[学成在线] day16:基于Spring Security Oauth2开发认证服务
😎 知識點概覽
為了方便后續回顧該項目時能夠清晰的知道本章節講了哪些內容,并且能夠從該章節的筆記中得到一些幫助,所以在完成本章節的學習后在此對本章節所涉及到的知識點進行總結概述。
本章節為【學成在線】項目的 day16 的內容
- 學習 Spring Security + Oauth2 基本概念以及實現過程。
- 學習 Oauth2 的基本應用場景,這里主要是通過 Oauth2 的密碼模式來實戰。
- 初識 JWT 令牌。
- 本章節的最后通過 Spring Security Oauth2 完成了認證服務的基本實現,但授權還沒做。
目錄
內容會比較多,小伙伴們可以根據目錄進行按需查閱。
文章目錄
- 😎 知識點概覽
- 目錄
- 一、用戶需求分析
- 0x01 用戶認證與授權
- 0x02 單點登錄需求
- 0x03 第三方認證需求
- 二、用戶認證技術方案
- 0x01 單點登錄技術方案
- 0x02 Oauth2認證
- 認證流程
- Oauth2在本項目的應用
- 0x03 Spring Security Oauth2 認證解決方案
- 三、Spring Security Oauth2 研究
- 0x01 目標
- 0x02 搭建認證服務器
- 導入基礎工程
- 創建數據庫
- 0x03 Oauth2授權碼模式
- 授權碼授權流程
- 申請授權碼
- 申請令牌
- 資源服務授權
- 1)授權流程
- 2)授權配置
- 3)授權測試
- 4)解決swagger-ui無法訪問
- 0x04 Oauth2密碼模式認證
- 0x05 校驗令牌
- 0x06 刷新令牌
- 0x07 JWT研究
- JWT介紹
- 令牌結構
- JWT入門
- 生成私鑰和公鑰
- 生成jwt令牌
- 驗證jwt令牌
- 四、認證服務開發
- 0x01 需求分析
- 0x02 Redis配置
- 安裝Redis服務
- redis連接配置
- 測試
- 0x03 認證服務
- 需求分析
- Api接口
- 配置參數
- 申請令牌測試
- Dao
- Service
- Controller
- 登錄url放行
- 測試認證接口
- 測試寫入Cookie
- 五、一些需要注意的問題
- 通用工程的依賴繼承的問題
- 😁 認識作者
一、用戶需求分析
0x01 用戶認證與授權
截至目前,項目已經完成了在線學習功能,用戶通過在線學習頁面點播視頻進行學習。
如何去記錄學生的學習過程呢?
要想掌握學生的學習情況就需要知道用戶的身份信息,記錄哪個用戶在什么時間學習什么課程;如果用戶要購買課程也需要知道用戶的身份信息。所以,去管理學生的學習過程最基本的要實現用戶的身份認證。
什么是用戶身份認證?
用戶身份認證即用戶去訪問 系統資源 時系統要求驗證用戶的身份信息,身份合法方可繼續訪問。常見的用戶身份認證表現形式有:用戶名密碼登錄,指紋打卡等方式。
什么是用戶授權?
用戶認證通過后去訪問系統的資源,系統會判斷用戶是否擁有訪問資源的 權限,只允許訪問有權限的系統資源,沒有權限的資源將無法訪問,這個過程叫用戶授權。
0x02 單點登錄需求
本項目包括多個子項目,如:學習系統,教學管理中心、系統管理中心等,為了提高用戶體驗性需要實現用戶只認證一次便可以在多個擁有訪問權限的系統中訪問,這個功能叫做單點登錄。
引用百度百科:單點登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一。
SSO 的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。
下圖是 SSO 的示意圖,用戶登錄學成網一次即可訪問多個系統。
0x03 第三方認證需求
作為互聯網項目難免需要訪問外部系統的資源,同樣本 服務 也要訪問 其他服務 的資源接口。
一個微信用戶沒有在學成在線注冊,本系統可以通過請求微信系統來驗證該用戶的身份,驗證通過后該用戶便可在本系統學習,它的基本流程如下:
從上圖可以看出,微信不屬于本系統,本系統并沒有存儲微信用戶的賬號、密碼等信息,本系統如果要獲取該用戶的基本信息則需要首先通過微信的認證系統(微信認證)進行認證,微信認證通過后本系統便可獲取該微信用戶的基本信息,從而在本系統將該微信用戶的頭像、昵稱等信息顯示出來,該用戶便不用在本系統注冊卻可以直接學習。
什么是第三方認證(跨平臺認證)?
當需要訪問第三方系統的資源時需要首先通過第三方系統的認證(例如:微信認證),由第三方系統對用戶認證通過,并授權資源的訪問權限。
二、用戶認證技術方案
0x01 單點登錄技術方案
分布式系統要實現單點登錄,通常將認證系統獨立抽取出來,并且將用戶身份信息存儲在單獨的存儲介質,比如:MySQL、Redis,考慮性能要求,通常存儲在 Redis 中,如下圖:
單點登錄的特點是:
1、認證系統為獨立的系統。
2、各個 子系統 通過 Http 或其它協議與認證系統通信,完成用戶認證。
3、用戶身份信息存儲在 Redis 集群。
Java 中有很多用戶認證的框架都可以實現單點登錄:
1、Apache Shiro.
2、CAS
3、Spring security CAS
0x02 Oauth2認證
認證流程
第三方認證技術方案最主要是解決認證協議的通用標準 問題,因為要實現 跨系統認證,各系統之間要遵循一定的接口協議。
OAUTH 協議為用戶資源的授權提供了一個安全的、開放而又簡易的標準。同時,任何第三方都可以使用 OAUTH 認證服務,任何服務提供商都可以實現自身的 OAUTH 認證服務,因而 OAUTH 是開放的。業界提供了 OAUTH 的多種實現如 PHP、JavaScript,Java,Ruby 等各種語言開發包,大大節約了程序員的時間,因而 OAUTH 是簡易的?;ヂ摼W很多服務如 Open API,很多大公司如 Google,Yahoo,Microsoft 等都提供了 OAUTH 認證服務,這些都足以說明 OAUTH 標準逐漸成為開放資源授權的標準。
Oauth 協議目前發展到 2.0 版本,1.0 版本過于復雜,2.0 版本已得到廣泛應用。
參考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth協議:https://tools.ietf.org/html/rfc6749
下邊分析一個Oauth2認證的例子,黑馬程序員網站使用微信認證的過程:
從流程圖可以看出,用戶首先需要訪問黑馬程序員的登錄頁面,登錄頁面中會有一個第三方登錄的選項,例如選擇微信來進行登錄。
點擊微信登錄后,黑馬程序員網站會向微信獲取到一個認證授權的頁面,并返回給客戶端,客戶端自動跳轉到該 認證授權頁面 進行微信的認證,當用戶通過微信授權認證成功后,微信的認證服務器會返回一個授權碼到客戶端,客戶端使用授權碼向微信認證服務器申請 認證token,當用戶獲取到 token 后,會攜帶該 token 值去請求黑馬程序員網站,黑馬程序員通過該token向微信服務器獲取到用戶的微信信息后,黑馬程序員網站才能確定該用戶是可信的。
具體流程演示如下:
1、客戶端請求第三方授權
用戶進入黑馬程序的登錄頁面,點擊微信的圖標以微信賬號登錄系統,用戶是自己在微信里信息的資源擁有者。
點擊“微信”出現一個二維碼,此時用戶掃描二維碼,開始給黑馬程序員授權。
2、資源擁有者同意給客戶端授權
資源擁有者掃描二維碼表示資源擁有者同意給客戶端授權,微信會對資源擁有者的身份進行驗證, 驗證通過后,微信會詢問用戶是否給授權黑馬程序員訪問自己的微信數據,用戶點擊 “確認登錄”表示同意授權,微信認證服務器會頒發一個授權碼,并重定向到黑馬程序員的網站。
3、客戶端獲取到授權碼,請求認證服務器申請令牌
此過程用戶看不到,客戶端應用程序請求認證服務器,請求攜帶授權碼。
4、認證服務器向客戶端響應令牌
認證服務器驗證了客戶端請求的授權碼,如果合法則給客戶端頒發令牌,令牌是客戶端訪問資源的通行證。此交互過程用戶看不到,當客戶端拿到令牌后,用戶在黑馬程序員看到已經登錄成功。
5、客戶端請求資源服務器的資源
客戶端攜帶令牌訪問資源服務器的資源。黑馬程序員網站攜帶令牌請求訪問微信服務器獲取用戶的基本信息。
6、資源服務器返回受保護資源
資源服務器校驗令牌的合法性,如果合法則向用戶響應資源信息內容。
注意:資源服務器和認證服務器可以是一個服務也可以分開的服務,如果是分開的服務資源服務器通常要請求認證服務器來校驗令牌的合法性。
Oauth2.0 認證流程如下:
引自 Oauth2.0 協議 rfc6749 https://tools.ietf.org/html/rfc6749
Oauth2包括以下角色:
1、客戶端
本身不存儲資源,需要通過資源擁有者的授權去請求資源服務器的資源,比如:學成在線Android客戶端、學成在線Web客戶端(瀏覽器端)、微信客戶端等。
2、資源擁有者
通常為用戶,也可以是應用程序,即該資源的擁有者。
3、授權服務器(也稱認證服務器)
用來對資源擁有的身份進行認證、對訪問資源進行授權。客戶端要想訪問資源需要通過認證服務器由資源擁有者授權后方可訪問。
4、資源服務器
存儲資源的服務器,比如,學成網用戶管理服務器存儲了學成網的用戶信息,學成網學習服務器存儲了學生的學習信息,微信的資源服務存儲了微信的用戶信息等??蛻舳俗罱K訪問資源服務器獲取資源信息。
Oauth2在本項目的應用
Oauth2是一個標準的開放的授權協議,應用程序可以根據自己的要求去使用 Oauth2,本項目使用 Oauth2 實現如下目標:
1、學成在線訪問第三方系統的資源
2、外部系統訪問學成在線的資源
3、學成在線前端(客戶端) 訪問學成在線微服務的資源。
4、學成在線微服務之間訪問資源,例如:微服務A 訪問 微服務B 的資源,B 訪問 A 的資源。
0x03 Spring Security Oauth2 認證解決方案
本項目采用 Spring security + Oauth2 完成用戶認證及用戶授權,Spring security 是一個強大的和高度可定制的身份驗證和訪問控制框架,Spring security 框架集成了Oauth2 協議,下圖是項目認證架構圖:
1、用戶請求認證服務完成認證。
2、認證服務下發用戶身份令牌,擁有身份令牌表示身份合法。
3、用戶攜帶令牌請求資源服務,請求資源服務必先經過網關。
4、網關校驗用戶身份令牌的合法,不合法表示用戶沒有登錄,如果合法則放行繼續訪問。
5、資源服務獲取令牌,根據令牌完成授權。
6、資源服務完成授權則響應資源信息。
三、Spring Security Oauth2 研究
0x01 目標
本項目認證服務基于 Spring Security Oauth2 進行構建,并在其基礎上作了一些擴展,采用 JWT 令牌機制,并自定義了用戶身份信息的內容。 本教程的主要目標是學習在項目中集成Spring Security Oauth2 的方法和流程,通過 Spring Security Oauth2 的研究需要達到以下目標:
1、理解 Oauth2 的授權碼認證流程及密碼認證的流程。
2、理解 Spring Security Oauth2 的工作流程。
3、掌握資源服務集成 Spring Security 框架完成 Oauth2 認證的流程。
0x02 搭建認證服務器
導入基礎工程
導入 資料 目錄下的 xc-service-ucenter-auth 工程,該工程是基于Spring Security Oauth2 的一個二次封裝的工程,導入此工程研究 Oauth2 認證流程。
創建數據庫
導入資料目錄下的 xc_user.sql,創建用戶數據庫
以 oauth_ 開頭的表都是 Spring Security 自帶的表。
本項目中 Spring Security 主要使用 oauth_client_details 表:
-
client_id:客戶端id
-
resource_ids:資源id(暫時不用)
-
client_secret:客戶端密碼
-
scope:范圍
-
access_token_validity:訪問token的有效期(秒)
-
refresh_token_validity:刷新token的有效期(秒)
-
authorized_grant_type:授權類型,
- authorization_code
- password
- refresh_token
- client_credentials
0x03 Oauth2授權碼模式
Oauth2 有以下授權模式:
-
授權碼模式(Authorization Code)
-
隱式授權模式(Implicit)
-
密碼模式(Resource Owner PasswordCredentials)
-
客戶端模式(Client Credentials)
其中授權碼模式和密碼模式應用較多,本小節介紹授權碼模式。
授權碼授權流程
上邊例舉的黑馬程序員網站使用微信認證的過程就是授權碼模式,流程如下:
1、客戶端請求第三方授權
2、用戶(資源擁有者)同意給客戶端授權
3、客戶端獲取到授權碼,請求認證服務器申請令牌
4、認證服務器向客戶端響應令牌
5、客戶端請求資源服務器的資源,資源服務校驗令牌合法性,完成授權
6、資源服務器返回受保護資源
申請授權碼
請求認證服務獲取授權碼:
GET 請求:
localhost:40400/auth/oauth/authorize? client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost參數列表如下:
- client_id:客戶端 id,和授權配置類中設置的客戶端id一致。
- response_type:授權碼模式固定為 code
- scop:客戶端范圍,和授權配置類中設置的 scop一致。
- redirect_uri:跳轉 uri,當授權碼申請成功后會跳轉到此地址,并在后邊帶上code參(授權碼)。
首次訪問會跳轉到登錄頁面:
輸入賬號和密碼,點擊 Login。
Spring Security 接收到請求會調用 UserDetailsService 接口的 loadUserByUsername 方法查詢用戶正確的密碼。
在 oauth_client_details 表中配置認證的賬號和密碼,當然密碼是加密后儲存的,這里我們暫時先不關注,后面再講解
賬號密碼為 XcWebApp 和 XcWebApp
接下來進入授權頁面:
點擊 同意,接下來返回授權碼:認證服務攜帶授權碼跳轉 redirect_uri
申請令牌
拿到授權碼后,申請令牌。
POST 請求:http://localhost:40400/auth/oauth/token
參數如下:
- grant_type:授權類型,填寫authorization_code,表示授權碼模式
- code:授權碼,就是剛剛獲取的授權碼,注意:授權碼只使用一次就無效了,需要重新申請。
- redirect_uri:申請授權碼時的跳轉url,一定和申請授權碼時用的redirect_uri一致。
此鏈接需要使用 http Basic認證。
什么是 http Basic認證?
http 協議定義的一種認證方式,將客戶端id和客戶端密碼按照 客戶端ID:客戶端密碼 的格式拼接,并用 base64 編碼,放在 header 中請求服務端,一個例子:
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=
WGNXZWJBcHA6WGNXZWJBcHA= 是 用戶名:密碼 的 base64 編碼。
如果認證失敗服務端會返回 401 Unauthorized
以上測試使用 postman 完成。
http basic認證:
客戶端 Id 和客戶端密碼會匹配數據庫 oauth_client_details 表中的客戶端 id 及客戶端密碼。
POST 請求參數:
點擊發送:
申請令牌成功。
- access_token:訪問令牌,攜帶此令牌訪問資源
- token_type:有 MAC Token與 Bearer Token兩種類型,兩種的校驗算法不同,RFC 6750建議Oauth2采用 Bearer
- Token(http://www.rfcreader.com/#rfc6750)。
- refresh_token:刷新令牌,使用此令牌可以延長訪問令牌的過期時間。
- expires_in:過期時間,單位為秒。
- scope:范圍,與定義的客戶端范圍一致。
資源服務授權
1)授權流程
資源服務擁有要訪問的受保護資源,客戶端攜帶令牌訪問資源服務,如果令牌合法則可成功訪問資源服務中的資源,流程如下圖:
上圖的業務流程如下:
1、客戶端請求認證服務申請令牌
2、認證服務生成令牌認證服務采用非對稱加密算法,使用私鑰生成令牌。
3、客戶端攜帶令牌訪問資源服務客戶端在 Http header 中添加: Authorization:Bearer 令牌。
注意這里的Authorization字段的值為 Bearer + 空格 + 令牌
4、資源服務請求認證服務校驗令牌的有效性資源服務接收到令牌,使用公鑰校驗令牌的合法性。
5、令牌有效,資源服務向客戶端響應資源信息
2)授權配置
基本上所有微服務都是資源服務,這里我們在 課程管理服務 上配置授權控制,當配置了授權控制后如要訪問課程信息則必須提供令牌。
在我們導入的 auth 工程的 resources 下可以看到一個 xc.keystore 文件,該文件是用于認證的一個私鑰文件,用于生成我們的授權碼,生成的授權碼可以使用 公鑰 文件來進行校驗。下面我們來做一個簡單的實驗來了解整個校驗的流程。
1、配置公鑰
認證服務生成令牌采用非對稱加密算法,認證服務采用私鑰加密生成令牌,對外向資源服務提供公鑰,資源服務使
用公鑰 來校驗令牌的合法性。
將 day16 的 資料 下的公鑰拷貝到 publickey.txt 文件中,將此文件拷貝到資源服務工程的 classpath 下
2、添加依賴
<!--oatuh2--> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>4、在 config 包下創建 ResourceServerConfig 類:
package com.xuecheng.manage_course.config;@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公鑰private static final String PUBLIC_KEY = "publickey.txt";//定義JwtTokenStore,使用jwt令牌@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}//定義JJwtAccessTokenConverter,使用jwt令牌@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setVerifierKey(getPubKey());return converter;}/*** 獲取非對稱加密公鑰 Key* @return 公鑰 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY);try {InputStreamReader inputStreamReader = newInputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader);return br.lines().collect(Collectors.joining("\n"));} catch (IOException ioe) {return null;}}//Http安全配置,對每個到達系統的http請求鏈接進行校驗@Overridepublic void configure(HttpSecurity http) throws Exception {//所有請求必須認證通過http.authorizeRequests().anyRequest().authenticated();} }3)授權測試
這里我們使用 POSTMAN 測試課程圖片查詢
GET http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000
請求時沒有攜帶令牌則報錯:
{ "error": "unauthorized", "error_description": "Full authentication is required to access this resource" }請求時攜帶令牌:
在 http header 中添加 Authorization: Bearer 令牌
當輸入錯誤的令牌也無法正常訪問資源。
4)解決swagger-ui無法訪問
這個問題可以單獨提取出來,發布到csdn上。
當課程管理加了授權之后再訪問 swagger-ui 則報錯
修改授權配置類 ResourceServerConfig 的 configure 方法:
針對 swagger-ui 的請求路徑進行放行:
//Http安全配置,對每個到達系統的http請求鏈接進行校驗 @Override public void configure(HttpSecurity http) throws Exception {//所有請求必須認證通過http.authorizeRequests()//下邊的路徑放行.antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui","/swagger-resources","/swagger-resources/configuration/security","/swagger-ui.html","/webjars/**").permitAll().anyRequest().authenticated(); }注意:
通過上邊的配置雖然可以訪問 swagger-ui,但是無法進行單元測試,除非去掉認證的配置或在上邊配置中添加所有請求均放行("/**")。
但是需要注意的是,雖說在開發環境下我們可與通過使用 /** 的方式來便于我們進行單元測試,但是難免會有疏漏的時候,如果在生產上線時沒有及時改回來,那么后果不堪設想。
所以我們可以考慮使用多環境配置的形式,將需要放行的 url 從配置文件 application.yml 中讀取,而開發環境中,我們可以單獨配置一個 application-dev.yml 作為我們的開發環境的配置,用于區別上線環境的配置。例如下面的例子
application-dev.yml 加入 oauth2.urlMatchers ,在該字段下指定我們無需授權訪問的一些url地址,使用 , 進行分割
oauth2:urlMatchers: /v2/api-docs,/swagger-resources/configuration/ui,/swagger-resources,/swagger-resources/configuration/security,/swagger-ui.html,/webjars/**而在生產環境中的配置 application-dev.yml 你可以不配置 urlMatchers 的值,這樣所有的url都需要認證后才能訪問,當然,你也可以添加一些例外。如下則是全部 url 都攔截的情況的配置,雖然沒有值,但是我們也要在配置文件中寫出這個 urlMatchers 字段,便于后面的拓展工作。
oauth2:urlMatchers:那么我們的配置類 ResourceServerConfig 就應該做出如下的修改:
使用 @Value 注解獲取配置文件中 urlMatchers 的值,在 configure 方法下做出相關的操作。
@Value("${oauth2.urlMatchers}") String urlMatchers;//Http安全配置,對每個到達系統的http請求鏈接進行校驗 @Override public void configure(HttpSecurity http) throws Exception {if(urlMatchers.equals("")){//如果urlMatchers未指定,則所有url都需要授權后才能被訪問http.authorizeRequests().anyRequest().authenticated();}else{//放行 urlMatchers 中指定的url條目, 未指定的url仍需授權后才能訪問String[] split = urlMatchers.split(",");http.authorizeRequests()//下邊的路徑放行.antMatchers(split).permitAll().anyRequest().authenticated();} }0x04 Oauth2密碼模式認證
密碼模式(Resource Owner Password Credentials)與授權碼模式的區別是申請令牌不再使用授權碼,而是直接
通過用戶名和密碼即可申請令牌。
測試如下:
POST 請求:http://localhost:40400/auth/oauth/token
參數:
- grant_type:密碼模式授權填寫password
- username:賬號
- password:密碼
那么這個密碼信息是從哪里獲取到的?
在我們的 auth 服務工程中可以看到,我們寫了一個 UserDetailsService 的實現類。
這個實現類中實現了 loadUserByUsername 方法,在該方法中,首先會驗證提交請求中帶有的 App 用戶密碼信息是否正確,也就是我們提交的 http Basic 認證信息,App的認證信息通過后,會出數據庫獲取用戶的認證信息和權限信息進行設置,然后再根據我們提交的信息進行比對。但在當前的測試中,我們是直接在 loadUserByUsername 方法內自定義了一個賬號和密碼,便于我們測試,完整的認證授權流程會在后面的內容中講到。
//設置用戶的認證和權限信息 XcUserExt userext = new XcUserExt(); userext.setUsername("mrt"); userext.setPassword(new BCryptPasswordEncoder().encode("123")); userext.setPermissions(new ArrayList<XcMenu>()); if(userext == null){ return null; }使用 http Basic 進行App的身份認證,這里我們的賬號和密碼都為 XcWebApp。
上邊參數使用 x-www-form-urlencoded 方式傳輸,使用postman測試如下:
那么,授權碼模式與密碼模式,分別都適用于哪些場景?
- 授權碼模式一般適用于提供給第三方進行認證,例如在前面提到的在黑馬程序員網站進行微信登錄時,這里我們的角色就應該對應的是 微信的認證服務器,而黑馬程序員網站屬于第三方。
- 密碼模式在我們后續的開發中會經常用到,一般用于我們微服務間的認證以及用戶的前臺、后臺權限管理等場景。
0x05 校驗令牌
Spring Security Oauth2 提供校驗令牌的端點,如下:
GET: http://localhost:40400/auth/oauth/check_token?token=
參數:
- token:令牌
使用 postman 測試如下:
結果如下:
{"companyId": null,"userpic": null,"user_name": "itcast","scope": ["app"],"name": null,"utype": null,"active": true,"id": null,"exp": 1590351690,"jti": "ed441eb3-cd16-4e74-b598-484656a03287","client_id": "XcWebApp" }-
exp:過期時間,long類型,距離1970年的秒數(new Date().getTime()可得到當前時間距離1970年的毫秒數)。
-
user_name: 用戶名
-
client_id:客戶端Id,在oauth_client_details中配置
-
scope:客戶端范圍,在oauth_client_details表中配置
-
jti:與令牌對應的唯一標識
-
companyId、userpic、name、utype、id:這些字段是本認證服務在Spring Security基礎上擴展的用戶身份信息
我們可以根據這些數據進行一些相關的操作。
0x06 刷新令牌
刷新令牌是當令牌快過期時重新生成一個令牌,它于授權碼授權和密碼授權生成令牌不同,刷新令牌不需要授權碼
也不需要賬號和密碼,只需要一個 刷新令牌、客戶端id 和 客戶端密碼。
測試如下:
POST:http://localhost:40400/auth/oauth/token
參數:
- grant_type: 固定為 refresh_token
- refresh_token:刷新令牌(注意不是 access_token,而是 refresh_token)
刷新令牌成功,會重生成新的訪問令牌和刷新令牌,令牌的有效期也比舊令牌長。
刷新令牌通常是在令牌快過期時進行刷新。
0x07 JWT研究
JWT介紹
在介紹JWT之前先看一下傳統校驗令牌的方法,如下圖:
問題:
傳統授權方法的問題是用戶每次請求資源服務,資源服務都需要攜帶令牌訪問認證服務去校驗令牌的合法性,并根
據令牌獲取用戶的相關信息,性能低下。
解決:
使用 JWT 的思路是,用戶認證通過會得到一個 JWT 令牌,JWT 令牌中已經包括了用戶相關的信息,客戶端只需要攜帶 JWT 訪問資源服務,資源服務根據事先約定的算法自行完成令牌校驗,無需每次都請求認證服務完成授權。JWT 令牌授權過程如下圖:
什么是 JWT ?
JSON Web Token(JWT)是一個開放的行業標準(RFC 7519),它定義了一種簡介的、自包含的協議格式,用于
在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT 可以使用 HMAC 算法或使用 RSA的公鑰/私鑰對來簽名,防止被篡改。
官網:https://jwt.io/
標準:https://tools.ietf.org/html/rfc7519
JWT 令牌的優點:
1、jwt基于 json,非常方便解析。
2、可以在令牌中自定義豐富的內容,易擴展。
3、通過非對稱加密算法及數字簽名技術,JWT 防止篡改,安全性高。
4、資源服務使用JWT可不依賴認證服務即可完成授權。
缺點:JWT令牌較長,占存儲空間比較大。
令牌結構
通過學習JWT令牌結構為自定義 jwt 令牌打好基礎。
JWT 令牌由三部分組成,每部分中間使用點(.)分隔,比如:xxxxx.yyyyy.zzzzz
1、Header
頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
下邊是Header部分的內容
2、Payload
第二部分是用于儲存一些有效信息,內容也是一個 json對象,它可以存放 jwt 提供的現成字段,比如:iss(簽發者),exp(過期時間戳), sub(面向的用戶)等,也可自定義字段。
此部分不建議存放敏感信息,因為此部分可以解碼還原原始內容。
最后將第二部分負載使用 Base64Url 編碼,得到一個字符串就是JWT令牌的第二部分。
一個例子 :
{"sub": "1234567890","name": "456","admin": true }3、Signature
第三部分是簽名,此部分用于防止jwt內容被篡改。
這個部分使用 base64url 將前兩部分進行編碼,編碼后使用點(.)連接組成字符串,最后使用header中聲明
簽名算法進行簽名。
一個例子:
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)-
base64UrlEncode(header):jwt令牌的第一部分。
-
base64UrlEncode(payload):jwt令牌的第二部分。
-
secret:簽名所使用的密鑰。
JWT入門
Spring Security 提供對 JWT 的支持,本節我們使用 Spring Security 提供的 JwtHelper 來創建JWT令牌,校驗JWT令牌等操作。
生成私鑰和公鑰
JWT令牌生成采用非對稱加密算法
1、生成密鑰證書
下邊命令生成密鑰證書,采用RSA 算法每個證書包含公鑰和私鑰
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystoreKeytool 是一個 java 提供的證書管理工具,以下是參數的說明
-
-alias:密鑰的別名
-
-keyalg:使用的hash算法
-
-keypass:密鑰的訪問密碼
-
-keystore:密鑰庫文件名,xc.keystore保存了生成的證書
-
-storepass:密鑰庫的訪問密碼
查詢證書信息:
keytool -list -keystore xc.keystore刪除別名:
keytool -delete -alias xckey -keystore xc.keystore2、導出公鑰
openssl 是一個加解密工具包,這里使用openssl來導出公鑰信息。
安裝 openssl:http://slproweb.com/products/Win32OpenSSL.html
安裝完成后,配置 openssl 的 path 環境變量,本教程配置在 D:\OpenSSL-Win64\bin
在命令行進入 xc.keystore 文件所在目錄執行如下命令:
keytool -list -rfc --keystore xc.keystore | openssl x509 -inform pem -pubkey輸入密鑰庫密碼,如下圖:
復制生成出來的公鑰數據
將上邊的公鑰拷貝到文本文件中,合并為一行,換行會有換行符,所以盡可能的避免一些我的發生 ,可以用notepad++ 直接替換換行符 \n 如下圖,當然你也可以手動的合并成一行。
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi1pGvYqLcTG2dcKhrtisQgkB90iWaCwE4OriDwCLMdiUV2NViEn+r/jMbuIFCBtnB21yWZlIPnXjzcre/8HIUJy2dMWqP9NUhzoCzwdC1I9clZRVHTpe1H0eiaQY4BLxz5EScBZdr5u4Q0hT+t6D3t7qQg1MHxLBaFy2cdHQbmz5Ly/1mmnWBHmFgjbbNG7gfaO3jRCl7RbNVUfSjb6gN+MfpyLk/iXr5S8Qhc2X07hvtm09QEk3cl14tQkZkXAUk7rAl9kgPSKoKr4MAdiYEsVNplKd4LMs4S2AC0dYrhdIX754eo6u4Ehpe6v5hSsF2d3ZpuV7nJ6JDCNxo7tU9wIDAQAB-----END PUBLIC KEY-----生成jwt令牌
在認證工程創建測試類,測試jwt令牌的生成與驗證。
package com.xuecheng.auth;import com.alibaba.fastjson.JSON; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.ClassPathResource; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; import org.springframework.security.jwt.crypto.sign.RsaSigner; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.test.context.junit4.SpringRunner;import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; import java.util.HashMap; import java.util.Map;@SpringBootTest @RunWith(SpringRunner.class) public class JwtTest {//生成一個jwt令牌@Testpublic void testCreateJwt(){//證書文件String key_location = "xc.keystore";//密鑰庫密碼String keystore_password = "xuechengkeystore";//訪問證書路徑ClassPathResource resource = new ClassPathResource(key_location);//密鑰工廠KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray());//密鑰的密碼,此密碼和別名要匹配String keypassword = "xuecheng";//密鑰別名String alias = "xckey";//密鑰對(密鑰和公鑰)KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray());//私鑰RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();//定義payload信息Map<String, Object> tokenMap = new HashMap<>();tokenMap.put("id", "123");tokenMap.put("name", "mrt");tokenMap.put("roles", "r01,r02");tokenMap.put("ext", "1");//生成jwt令牌Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate));//取出jwt令牌String token = jwt.getEncoded();System.out.println(token);} }驗證jwt令牌
//資源服務使用公鑰驗證jwt的合法性,并對jwt解碼 @Test public void testVerify(){//jwt令牌String token ="";//公鑰String publickey = "";//校驗jwtJwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));//獲取jwt原始內容String claims = jwt.getClaims();System.out.println(claims);//jwt令牌String encoded = jwt.getEncoded();System.out.println(encoded); }四、認證服務開發
0x01 需求分析
用戶登錄的流程圖如下
執行流程:
1、用戶登錄,請求認證服務
2、認證服務認證通過,生成 jwt 令牌,將 jwt 令牌及相關信息寫入 Redis,并且將身份令牌寫入 cookie
3、用戶訪問資源頁面,帶著 cookie 到網關
4、網關從 cookie 獲取 token,并查詢 Redis 校驗 token,如果 token 不存在則拒絕訪問,否則放行
5、用戶退出,請求認證服務,清除 redis 中的 token,并且刪除 cookie 中的 token
使用 redis 存儲用戶的身份令牌有以下作用:
1、實現用戶退出注銷功能,服務端清除令牌后,即使客戶端請求攜帶 token 也是無效的。
2、由于 jwt 令牌過長,不宜存儲在 cookie 中,所以將 jwt 的 身份令牌 存儲在 redis,客戶端請求服務端時附帶這個 身份令牌,服務端根據身份令牌到 redis 中取出身份令牌對應的 jwt 令牌。
0x02 Redis配置
安裝Redis服務
下載Windows版本的redis:https://github.com/MicrosoftArchive/redis/tags
下載 msi 安裝包進行安裝
刷新服務,會看到多了一個 redis 服務。
如果下載的是zip包
運行
redis‐server redis.windows.conf注冊為服務:
redis‐server ‐‐service‐install redis.windows‐service.conf ‐‐loglevel verbose常用的 redis 服務命令如下:
卸載服務:sc delete Redis
開啟服務:net start Redis
停止服務:net stop Redis
下載 windows 版本的redis客戶端:https://redisdesktop.com/download
下載 redis-desktop-manager-0.9.2.806.exe
安裝后啟動 redis 客戶端:
配置 redis 鏈接:
連接成功
redis連接配置
在認證服務的 application.yml 文件中添加如下配置:
spring:application:name: xc‐service‐ucenter‐authredis:host: ${REDIS_HOST:127.0.0.1}port: ${REDIS_PORT:6379}timeout: 5000 #連接超時 毫秒jedis:pool:maxActive: 3maxIdle: 3minIdle: 1maxWait: ‐1 #連接池最大等行時間 ‐1沒有限制測試
@SpringBootTest @RunWith(SpringRunner.class) public class RedisTest {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void testRedis(){//定義keyString key = "user_token:9734b68f‐cf5e‐456f‐9bd6‐df578c711390";//定義MapMap<String,String> mapValue = new HashMap<>();mapValue.put("id","101");mapValue.put("username","itcast");String value = JSON.toJSONString(mapValue);//向redis中存儲字符串stringRedisTemplate.boundValueOps(key).set(value,60, TimeUnit.SECONDS);//讀取過期時間,已過期返回‐2Long expire = stringRedisTemplate.getExpire(key);//根據key獲取valueString s = stringRedisTemplate.opsForValue().get(key);System.out.println(s);} }0x03 認證服務
需求分析
認證服務需要實現的功能如下:
1、登錄接口
前端post提交賬號、密碼等,用戶身份校驗通過,生成令牌,并將令牌存儲到redis。
將令牌寫入cookie。
2、退出接口
校驗當前用戶的身份為合法并且為已登錄狀態。
將令牌從redis刪除。
刪除cookie中的令牌。
業務流程如下:
Api接口
@Api(value = "用戶認證",description = "用戶認證接口") public interface AuthControllerApi {@ApiOperation("登錄")public LoginResult login(LoginRequest loginRequest);@ApiOperation("退出")public ResponseResult logout(); }配置參數
在 application.yml 中配置參數
auth:tokenValiditySeconds: 1200 #token存儲到redis的過期時間clientId: XcWebAppclientSecret: XcWebAppcookieDomain: localhostcookieMaxAge: ‐1申請令牌測試
為了不破壞 Spring Security 的代碼,我們在 Service 方法中通過 RestTemplate 請求 Spring Security 所暴露的申請令
牌接口來申請令牌,下邊是測試代碼:
Dao
暫時使用靜態數據,待用戶登錄調通再連接數據庫校驗用戶信息。
Service
調用認證服務申請令牌,并將令牌存儲到 redis。
1、AuthToken
創建 AuthToken 模型類,存儲申請的令牌,包括身份令牌、刷新令牌、jwt令牌
身份令牌:用于校驗用戶是否認證
刷新令牌:jwt令牌快過期時執行刷新令牌
jwt令牌:用于授權
package com.xuecheng.framework.domain.ucenter.ext;import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString;/*** Created by mrt on 2018/5/21.*/ @Data @ToString @NoArgsConstructor public class AuthToken {String access_token;//訪問tokenString refresh_token;//刷新tokenString jwt_token;//jwt令牌 }申請令牌的 service 方法如下
這里要注意一點的就是,原視頻和講義中將用戶憑證儲存到 redis 的方法是直接儲存 access_token 作為 key,如果 access_token 作為key儲存的話,用戶在發送認證請求的時候就需要提供 access_token,也意味著 access_token 需要暴露給用戶。
-
直接暴露 access_token 會有一定的安全風險
-
access_token 長度太大,不適合儲存在cookie
前面的時候該課程的老師也講到了這個問題,但是后面還是犯了這個錯誤(至少我認為這是不應該的),所以 key 應該使用 jti 的值作為儲存 ,value 為 access_token + refresh_token 儲存到 map 轉換成json后的字符串。
package com.xuecheng.auth.service.impl;@Service public class AuthServiceImpl implements AuthService {private static final Logger LOGGER = LoggerFactory.getLogger(AuthService.class);@Value("${auth.tokenValiditySeconds}")int tokenValiditySeconds;//Eureka負載均衡客戶端@AutowiredLoadBalancerClient loadBalancerClient;@AutowiredRestTemplate restTemplate;@AutowiredStringRedisTemplate stringRedisTemplate;/*** 用戶登陸認證實現* @param username 用戶名* @param password 密碼* @param clientId 客戶端id* @param clientSecret 客戶端憑證* @return AuthToken*/@Overridepublic AuthToken login(String username, String password, String clientId, String clientSecret) {//申請令牌AuthToken authToken = this.appleToken(username, password, clientId, clientSecret);if(authToken == null){ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);}//保存令牌到redisboolean saveToken = this.saveToken(authToken, tokenValiditySeconds);if(!saveToken){ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL);}return authToken;}//儲存令牌到redisprivate boolean saveToken(AuthToken authToken, long ttl){//儲存到redis的keyString key = "user_token:" + authToken.getJwt_token();Map<String,String> valueMap = new HashMap<>();//拼裝valuevalueMap.put("access_token",authToken.getAccess_token());valueMap.put("refresh_token",authToken.getRefresh_token());String valueJson = JSON.toJSONString(valueMap);//保存到令牌到redisstringRedisTemplate.boundValueOps(key).set(valueJson,ttl, TimeUnit.SECONDS);//獲取過期時間Long expire = stringRedisTemplate.getExpire(key);//大于0則返回truereturn expire>0;}//向Oauth2服務申請令牌private AuthToken appleToken(String username, String password, String clientId, String clientSecret){//采用客戶端負載均衡的方式從eureka獲取認證服務的ip和端口ServiceInstance serviceInstance = loadBalancerClient.choose("XC-SERVICE-UCENTER-AUTH");URI uri = serviceInstance.getUri();String authUrl = uri + "/auth/oauth/token";//使用LinkedMultiValueMap儲存多個header信息LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();//設置basic認證信息String basicAuth = this.getHttpBasic(clientId, clientSecret);headers.add("Authorization",basicAuth);//設置請求中的body信息LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();body.add("grant_type","password");body.add("username",username);body.add("password",password);HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, headers);//憑證信息錯誤時候, 指定restTemplate當遇到400或401響應時候也不要拋出異常,也要正常返回值restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){@Overridepublic void handleError(ClientHttpResponse response) throws IOException {//當響應的值為400或者401時也要正常響應,不要拋出異常if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){super.handleError(response);}}});Map map = null;try {//http請求spring security的申請令牌接口ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(authUrl, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(body, headers), Map.class);map = mapResponseEntity.getBody();} catch (RestClientException e) {e.printStackTrace();LOGGER.error("request oauth_token_password error: {}",e.getMessage());e.printStackTrace();ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);}//校驗獲取到的jwt是否完成if(map == null ||map.get("access_token") == null ||map.get("refresh_token") == null ||map.get("jti") == null){//jti是jwt令牌的唯一標識作為用戶身份令牌ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);}//拼裝authToken并返回AuthToken authToken = new AuthToken();//訪問令牌(jwt)String access_token = (String) map.get("access_token");//刷新令牌(jwt)String refresh_token = (String) map.get("refresh_token");//jti,作為用戶的身份標識,也就是后面我們用于返回給到用戶前端的憑證String jwt_token = (String) map.get("jti");authToken.setAccess_token(access_token);authToken.setRefresh_token(refresh_token);authToken.setJwt_token(jwt_token);return authToken;}private String getHttpBasic(String clientId, String clientSecret){//將客戶端id和客戶端密碼拼接,按“客戶端id:客戶端密碼”String string = clientId+":"+clientSecret;//進行base64編碼byte[] encode = Base64.encode(string.getBytes());return "Basic "+new String(encode);} }Controller
package com.xuecheng.auth.controller;//在配置文件中設置了context-path: /auth 所以這里我們就不用再配置RequestMapping @RestController public class AuthController implements AuthControllerApi {//客戶端認證信息@Value("${auth.clientId}")String clientId;@Value("${auth.clientSecret}")String clientSecret;//cookie域@Value("${auth.cookieDomain}")String cookieDomain;//cookie生命周期@Value("${auth.cookieMaxAge}")int cookieMaxAge;//生命周期@Value("${auth.tokenValiditySeconds}")int tokenValiditySeconds;@AutowiredAuthService authService;/*** 用戶登陸接口* @param loginRequest 登陸參數* @return LoginResult*/@PostMapping("/userlogin")@Overridepublic LoginResult login(LoginRequest loginRequest) {//校驗賬號是否輸入if(loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())){ExceptionCast.cast(AuthCode.AUTH_USERNAME_NONE);}//校驗密碼是否輸入if(StringUtils.isEmpty(loginRequest.getPassword())){ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);}//獲取用戶token信息并且保存到redis內AuthToken authToken = authService.login(loginRequest.getUsername(),loginRequest.getPassword(), clientId, clientSecret);//將用戶token寫入cookieString jtw_token = authToken.getJwt_token();//將訪問令牌存儲到cookiethis.saveCookie(jtw_token);return new LoginResult(CommonCode.SUCCESS,jtw_token);}@Overridepublic ResponseResult logout() {return null;}//將令牌保存到cookieprivate void saveCookie(String token){HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();//添加cookie 認證令牌,最后一個參數設置為false,表示允許瀏覽器獲取CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, cookieMaxAge, false);} }登錄url放行
認證服務默認都要校驗用戶的身份信息,這里需要將登錄url放行。
在 WebSecurityConfig 類中重寫 configure(WebSecurity web)方法,如下:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin"); }測試認證接口
使用 postman 測試:
POST 請求:http://localhost:40400/auth/userlogin
測試寫入Cookie
cookie最終會寫到 xuecheng.com 域名下,可通過 nginx 代理進行認證,測試cookie是否寫成功。
1、配置nginx代理
在ucenter.xuecheng.com下配置代理路徑
#認證 location ^~ /openapi/auth/ {proxy_pass http://auth_server_pool/auth/; }添加
#認證服務 upstream auth_server_pool{server 127.0.0.1:40400 weight=10; }2、檢查我們的配置文件中domain的配置
domain 設置為我們學成的主站域名,xuecheng.com
auth:tokenValiditySeconds: 1200 #token存儲到redis的過期時間clientId: XcWebAppclientSecret: XcWebAppcookieDomain: xuecheng.comcookieMaxAge: -13、請求測試
http://ucenter.xuecheng.com/openapi/auth/userlogin
觀察 cookie 寫入結果
請求成功,cookie也成功拿到
五、一些需要注意的問題
通用工程的依賴繼承的問題
model 工程中構建 UserJwt 實體時候需要引入 oauth2 的依賴,所以在引入依賴時需要注意使用 optional 標簽防止其他服務工程繼承到 model 工程下的 oauth2 依賴。
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><optional>true</optional> </dependency>如果不配置 <optional>true</optional> 會導致暫時無需認證的服務也會被動的開啟了認證,并導致所有的接口都被認證攔截。
并且 model 、api 、common 這三個通用工程在引入本工程需要用到的一些依賴時,也務必加上<optional>true</optional> 這個標簽,防止依賴繼承導致的一些問題的出現。
😁 認識作者
作者:👦 LCyee ,全干型代碼🐕
自建博客:https://www.codeyee.com
記錄學習以及項目開發過程中的筆記與心得,記錄認知迭代的過程,分享想法與觀點。
CSDN 博客:https://blog.csdn.net/codeyee
記錄和分享一些開發過程中遇到的問題以及解決的思路。
歡迎加入微服務練習生的隊伍,一起交流項目學習過程中的一些問題、分享學習心得等,不定期組織一起刷題、刷項目,共同見證成長。
總結
以上是生活随笔為你收集整理的微服务[学成在线] day16:基于Spring Security Oauth2开发认证服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转载:企业关键业绩指标(KPI)与绩效考
- 下一篇: spring boot、SpringCl