教程揭秘 | 動(dòng)力節(jié)點(diǎn)內(nèi)部Java零基礎(chǔ)教學(xué)文檔第十八篇:SpringSecurity
接上期后續(xù)
本期分享第十八章節(jié)
SpringSecurity
文檔馬上就分享完了,你們都跟上了嗎?
每天都在學(xué)習(xí)嘛?
有什么不會(huì)的嘛?
今日教學(xué)文檔分享來了?

今日新篇章
【SpringSecurity】
1.?認(rèn)證授權(quán)的基礎(chǔ)概念
1.1?什么是認(rèn)證(登錄)
進(jìn)入移動(dòng)互聯(lián)網(wǎng)時(shí)代,大家每天都在刷手機(jī),常用的軟件有微信、支付寶、頭條等,下邊拿微信來舉例子說明認(rèn)證相關(guān)的基本概念,在初次使用微信前需要注冊成為微信用戶,然后輸入賬號和密碼即可登錄微信,輸入賬號和密碼登錄微信的過程就是認(rèn)證。
系統(tǒng)為什么要認(rèn)證?
http://127.0.0.1:8080/getAllUser
http://127.0.0.1:8080/addUser
http://127.0.0.1:8080/updateUser
http://127.0.0.1:8080/deleteUser
?
認(rèn)證是為了保護(hù)系統(tǒng)的隱私數(shù)據(jù)與資源,用戶的身份合法方可訪問該系統(tǒng)的資源。
認(rèn)證 :用戶認(rèn)證就是判斷一個(gè)用戶的身份是否合法的過程,用戶去訪問系統(tǒng)資源(url接口)時(shí)系統(tǒng)要求驗(yàn)證用戶的身份信息,身份合法方可繼續(xù)訪問,不合法則拒絕訪問。常見的用戶身份認(rèn)證方式有:用戶名密碼登錄,二維碼登錄,手機(jī)短信登錄,指紋認(rèn)證等方式。
1.2?什么是會(huì)話
HttpSession
SqlSession
用戶認(rèn)證通過后,為了避免用戶的每次操作都進(jìn)行認(rèn)證可將用戶的信息保證在會(huì)話中。會(huì)話就是系統(tǒng)為了保持當(dāng)前用戶的登錄狀態(tài)所提供的機(jī)制,常見的有基于session方式、基于token方式等。
1.2.1?基于session的認(rèn)證方式
它的交互流程是,用戶認(rèn)證成功后,在服務(wù)端生成用戶相關(guān)的數(shù)據(jù)保存在session(當(dāng)前會(huì)話)中,發(fā)給客戶端的sesssion_id 存放到 cookie 中,這樣用戶客戶端請求時(shí)帶上 session_id 就可以驗(yàn)證服務(wù)器端是否存在 session 數(shù)據(jù),以此完成用戶的合法校驗(yàn),當(dāng)用戶退出系統(tǒng)或session過期銷毀時(shí),客戶端的session_id也就無效了
1.2.2?基于token方式認(rèn)證方式
它的交互流程是,用戶認(rèn)證成功后,服務(wù)端生成一個(gè)token發(fā)給客戶端,客戶端可以放到 cookie 或 localStorage等存儲中,每次請求時(shí)帶上 token,服務(wù)端收到token通過驗(yàn)證后即可確認(rèn)用戶身份。Redis 存的用戶信息 ?共享session?(分布式中)
基于session的認(rèn)證方式由Servlet規(guī)范定制,服務(wù)端要存儲session信息需要占用內(nèi)存資源,客戶端需要支持cookie;基于token的方式則一般不需要服務(wù)端存儲token,并且不限制客戶端的存儲方式。如今移動(dòng)互聯(lián)網(wǎng)時(shí)代更多類型的客戶端需要接入系統(tǒng),系統(tǒng)多是采用前后端分離的架構(gòu)進(jìn)行實(shí)現(xiàn),所以基于token的方式更適合。
1.3?什么是授權(quán)?(給用戶頒發(fā)權(quán)限)
還拿微信來舉例子,微信登錄成功后用戶即可使用微信的功能,比如,發(fā)紅包、發(fā)朋友圈、添加好友等,沒有綁定銀行卡的用戶是無法發(fā)送紅包的,綁定銀行卡的用戶才可以發(fā)紅包,發(fā)紅包功能、發(fā)朋友圈功能都是微信的資源即功能資源,用戶擁有發(fā)紅包功能的權(quán)限才可以正常使用發(fā)送紅包功能,擁有發(fā)朋友圈功能的權(quán)限才可以使用發(fā)朋友圈功能,這個(gè)根據(jù)用戶的權(quán)限來控制用戶使用資源的過程就是授權(quán)。?鑒權(quán)(判斷用戶是否有這個(gè)權(quán)限)
java應(yīng)用中什么叫資源 url就是資源 ?(API接口就是資源)
http://127.0.0.1:8080/user/getUserById?id=10
1.3.1?為什么要授權(quán)?(控制資源(url)被訪問)
認(rèn)證是為了保證用戶身份的合法性,授權(quán)則是為了更細(xì)粒度的對隱私數(shù)據(jù)進(jìn)行劃分,授權(quán)是在認(rèn)證通過后發(fā)生的,控制不同的用戶能夠訪問不同的資源。
授權(quán):?授權(quán)是用戶認(rèn)證通過根據(jù)用戶的權(quán)限來控制用戶訪問資源的過程,擁有資源的訪問權(quán)限則正常訪問,沒有權(quán)限則拒絕訪問。
200
302
400
403
404
1.4?授權(quán)的數(shù)據(jù)模型(RBAC)
如何進(jìn)行授權(quán)即如何對用戶訪問資源進(jìn)行控制,首先需要學(xué)習(xí)授權(quán)相關(guān)的數(shù)據(jù)模型。
授權(quán)可簡單理解為Who對What(which)進(jìn)行How操作,包括如下:
Who,即主體(Subject),主體一般是指用戶,也可以是程序,需要訪問系統(tǒng)中的資源。 What,即資源(Resource),如系統(tǒng)菜單、頁面、按鈕、代碼方法、系統(tǒng)商品信息、系統(tǒng)訂單信息等。系統(tǒng)菜單、頁面、按鈕、代碼方法都屬于系統(tǒng)功能資源,對于web系統(tǒng)每個(gè)功能資源通常對應(yīng)一個(gè)URL;系統(tǒng)商品信息、系統(tǒng)訂單信息都屬于實(shí)體資源(數(shù)據(jù)資源),實(shí)體資源由資源類型和資源實(shí)例組成,比如商品信息為資源類型,商品編號 為001的商品為資源實(shí)例。
How,權(quán)限/許可(Permission),規(guī)定了用戶對資源的操作許可,權(quán)限離開資源沒有意義,如用戶查詢權(quán)限、用戶添加權(quán)限、某個(gè)代碼方法的調(diào)用權(quán)限、編號為001的用戶的修改權(quán)限等,通過權(quán)限可知用戶對哪些資源都有哪些操作可。
主體、資源、權(quán)限關(guān)系如下圖:
?

主體、資源、權(quán)限相關(guān)的數(shù)據(jù)模型如下:
主體(用戶id、賬號、密碼、...)
資源(資源id、資源名稱、訪問地址、...)
權(quán)限(權(quán)限id、權(quán)限標(biāo)識、權(quán)限名稱、資源id、...)
角色(角色id、角色名稱、...)
角色和權(quán)限關(guān)系(角色 id、權(quán)限id、...)
主體(用戶)和角色關(guān)系(用戶id、角色id、...)
主體(用戶)、資源、權(quán)限關(guān)系如下圖:
?

通常企業(yè)開發(fā)中將資源和權(quán)限表合并為一張權(quán)限表,如下:
資源(資源id、資源名稱、訪問地址、...)
權(quán)限(權(quán)限id、權(quán)限標(biāo)識、權(quán)限名稱、資源id、...)
合并為:
權(quán)限(權(quán)限id、權(quán)限標(biāo)識、權(quán)限名稱、資源名稱、資源訪問地址、...)
修改后數(shù)據(jù)模型之間的關(guān)系如下圖:
?

1.5?RBAC
用戶,角色,權(quán)限 本質(zhì):就是把權(quán)限打包給角色,分配給用戶
RBAC一般指基于角色的訪問控制???權(quán)限 ?五張表 (最少五張表)
基于角色的訪問控制(RBAC)是實(shí)施面向企業(yè)安全策略的一種有效的訪問控制方式。
1.5.1?基于角色的訪問控制
RBAC基于角色的訪問控制(Role-Based Access Control)是按角色進(jìn)行授權(quán),比如:主體的角色為總經(jīng)理可以查詢企業(yè)運(yùn)營報(bào)表,查詢員工工資信息等
根據(jù)上圖中的判斷邏輯,授權(quán)代碼可表示如下:
if(主體.hasRole("總經(jīng)理角色id")){
查詢工資
}
如果上圖中查詢工資所需要的角色變化為總經(jīng)理和部門經(jīng)理,此時(shí)就需要修改判斷邏輯為“判斷用戶的角色是否是總經(jīng)理或部門經(jīng)理”,修改代碼如下:
if(主體.hasRole("總經(jīng)理角色id") ||? 主體.hasRole("部門經(jīng)理角色id")){
? ? 查詢工資
}
根據(jù)上邊的例子發(fā)現(xiàn),當(dāng)需要修改角色的權(quán)限時(shí)就需要修改授權(quán)的相關(guān)代碼,系統(tǒng)可擴(kuò)展性差。
1.5.2?基于資源的訪問控制
RBAC基于資源的訪問控制(Resource-Based Access Control)是按資源(或權(quán)限)進(jìn)行授權(quán),比如:用戶必須具有查詢工資權(quán)限才可以查詢員工工資信息等,如下的判斷
if(主體.hasPermission("查詢工資") ){
? ? 查詢工資
}
優(yōu)點(diǎn):系統(tǒng)設(shè)計(jì)時(shí)定義好查詢工資的權(quán)限標(biāo)識,即使查詢工資所需要的角色變化為總經(jīng)理和部門經(jīng)理也不需要修改授權(quán)代碼,系統(tǒng)可擴(kuò)展性強(qiáng)。
2.?Spring Security 簡介
官網(wǎng): https://spring.io/projects/spring-security?
中文文檔: https://www.springcloud.cc/spring-security.html?
?

2.1?什么是SpringSecurity?
Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式(注解)的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應(yīng)用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉(zhuǎn)Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,為應(yīng)用系統(tǒng)提供聲明式的安全訪問控制功能,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復(fù)代碼的工作。
以上解釋來源于百度百科??梢砸痪湓拋砀爬?,SpringSecurity 是一個(gè)安全框架。
3.?Spring Security入門體驗(yàn)
3.1?創(chuàng)建父項(xiàng)目spring-security-main
把下面的項(xiàng)目都放在這個(gè)項(xiàng)目里面,方便展示和學(xué)習(xí)
?

3.2?創(chuàng)建項(xiàng)目security-hello并且選擇依賴
?

3.3?pom.xml
3.4?啟動(dòng)類
@SpringBootApplication
@EnableWebSecurity // 啟用security 在5.X版本之后可以不用加,默認(rèn)就是開啟的
public class SecurityHelloApplication {
????public static void main(String[] args) {
????????SpringApplication.run(SecurityHelloApplication.class, args);
????}
}
3.5?創(chuàng)建HelloController
@RestController
public class HelloController {
????@GetMapping("hello")
????public String hello() {
????????return "hello security";
????}
}
3.6?啟動(dòng)測試訪問
http://127.0.0.1:8080/hello?
發(fā)現(xiàn)我們無法訪問hello這個(gè)請求,這是因?yàn)閟pring Security默認(rèn)攔截了所有請求
?
使用user+啟動(dòng)日志里面的密碼登陸
?
?

登錄后就可以訪問我們的hello了
?


3.7?測試退出
訪問: http://localhost:8080/logout?
?
?


3.8?自定義密碼登錄(yml配置文件方式)
spring:
????security:
????????user:
????????????name: admin ????????#默認(rèn)使用的用戶名
????????????password: 123456 ???#默認(rèn)使用的密碼
3.9?重啟使用admin和123456登錄即可
3.10?總結(jié)
從上面的體驗(yàn)來說,是不是感覺很簡單,但是別急。后面的東西還是有點(diǎn)難度的,如下:
l?如何讀取數(shù)據(jù)庫的用戶名和密碼
l?如何對密碼加密
l?如何使用數(shù)據(jù)庫的角色和權(quán)限
l?如何配置方法級別的權(quán)限訪問
l?如何自定義登陸頁面
l?如何集成redis把登陸信息放到Redis
l?如何集成驗(yàn)證碼
l?……………………
4.?Spring Security配置多用戶認(rèn)證
4.1?概述
認(rèn)證就是登陸,我們現(xiàn)在沒有連接數(shù)據(jù)庫,那么我們可以模擬下用戶名和密碼
4.2?創(chuàng)建認(rèn)證的配置類WebSecurityConfig
4.3?啟動(dòng)測試
我們只要添加了安全配置類,那么我們在yml里面的配置就失效了
我們使用cxs/123訪問登錄,發(fā)現(xiàn)控制臺報(bào)錯(cuò)了
?

這個(gè)是因?yàn)閟pring Sercurity強(qiáng)制要使用密碼加密,當(dāng)然我們也可以不加密,但是官方要求是不管你是否加密,都必須配置一個(gè)類似Shiro的憑證匹配器
4.4?修改WebSecurityConfig添加加密器
4.5?重啟測試
兩個(gè)用戶都可以登錄成功了
4.6?測試加密和解密
我們對123字符串加密三次,然后匹配三次,看看效果
查看控制臺發(fā)現(xiàn)特點(diǎn)是:相同的字符串加密之后的結(jié)果都不一樣,但是比較的時(shí)候是一樣的
?
4.7?如何獲取當(dāng)前登錄用戶的信息(兩種方式)【重點(diǎn)】
我們添加獲取當(dāng)前用戶信息的Controller
?
測試訪問
http://localhost:8080/userInfo?
http://localhost:8080/userInfo2
?
5.?Spring Security用戶,角色,權(quán)限攔截配置
5.1?角色和權(quán)限的配置,修改WebSecurityConfig類
5.2?添加幾個(gè)Controller接口
5.3?啟動(dòng)測試
使用cxs/123登錄后 這幾個(gè)接口都可以訪問
使用test/123登錄后,訪問/update和/del會(huì)報(bào)錯(cuò)跳到403頁面
使用admin/123登錄后,只能訪問/admin/hello,訪問其他接口會(huì)跳到403頁面
5.4?我們添加一個(gè)403.html,讓他報(bào)錯(cuò)后跳到我們自己的頁面
?

5.5?重啟使用test登錄后訪問/del
?

6.?Spring Security方法級別的授權(quán)(鑒權(quán))
我們使用方法級別的授權(quán)后,只需要在controller對應(yīng)的方法上添加注解即可了,不需要再webSecurityConfig中配置匹配的url和權(quán)限了,這樣就爽多了
6.1?相關(guān)注解說明
@PreAuthorize? 在方法調(diào)用前進(jìn)行權(quán)限檢查
@PostAuthorize?在方法調(diào)用后進(jìn)行權(quán)限檢查
上面的兩個(gè)注解如果要使用的話必須加上
@EnableGlobalMethodSecurity(prePostEnabled = true)
如果只使用PreAuthorize?就只用開啟prePostEnabled = true
6.2?在WebSecurityConfig類上添加注解
?

6.3?注釋掉WebSecurityConfig配置url和權(quán)限的代碼
?

6.4?修改controller,給方法添加注解
不加注解的,都可以訪問,加了注解的,要有對應(yīng)權(quán)限才可以訪問哦
6.5?重啟測試即可
7.?Spring Security返回JSON(前后端分離)
在上面的例子中,我們返回的是403頁面,但是在開發(fā)中,如RestAPI風(fēng)格的數(shù)據(jù),是不能返回一個(gè)頁面,而應(yīng)該是給一個(gè)json
7.1?創(chuàng)建Result
?
7.2?創(chuàng)建登陸成功的處理器AppAuthenticationSuccessHandler
?
7.3?創(chuàng)建登陸失敗處理器AppAuthenticationFailureHandler
?
7.4?創(chuàng)建無權(quán)限處理器AppAccessDeniedHandler
?
7.5?創(chuàng)建登出處理器AppLogoutSuccessHandler
?
7.6?修改WebSecurityConfig出現(xiàn)拒接訪問走自己的處理器
?
7.7?重啟使用test/123登錄測試
?

8.?【源碼分析】Spring Security認(rèn)證授權(quán)總攬
目的:怎么自定義登錄
權(quán)限的控制 人家已經(jīng)寫的很好了
8.1?結(jié)構(gòu)總攬
? ??Spring Security所解決的問題就是安全訪問控制,而安全訪問控制功能其實(shí)就是對所有進(jìn)入系統(tǒng)的請求進(jìn)行攔截,校驗(yàn)每個(gè)請求是否能夠訪問它所期望的資源。根據(jù)前邊知識的學(xué)習(xí),可以通過Filter或AOP等技術(shù)來實(shí)現(xiàn),SpringSecurity對Web資源的保護(hù)是靠Filter實(shí)現(xiàn)的,所以從這個(gè)Filter來入手,逐步深入Spring Security原理。當(dāng)初始化Spring Security時(shí),會(huì)創(chuàng)建一個(gè)名為 SpringSecurityFilterChain 的Servlet過濾器,類型為org.springframework.security.web.FilterChainProxy,它實(shí)現(xiàn)了javax.servlet.Filter,因此外部的請求會(huì)經(jīng)過此類,下圖是Spring Security過慮器鏈結(jié)構(gòu)圖:
?

8.2?上圖說明
FilterChainProxy 是一個(gè)代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各個(gè)Filter,同時(shí)這些Filter作為Bean被Spring管理,它們是Spring Security核心,各有各的職責(zé),但他們并不直接處理用戶的認(rèn)證,也不直接處理用戶的授權(quán),而是把它們交給了認(rèn)證管理器(AuthenticationManager)和決策管理器(AccessDecisionManager)進(jìn)行處理
下圖是FilterChainProxy相關(guān)類的UML圖示
?
?

spring Security功能的實(shí)現(xiàn)主要是由一系列過濾器鏈相互配合完成。
?

?
8.3?過濾器鏈中主要的幾個(gè)過濾器及其作用
8.3.1?SecurityContextPersistenceFilter?
? ??這個(gè)Filter是整個(gè)攔截過程的入口和出口(也就是第一個(gè)和最后一個(gè)攔截器),會(huì)在請求開始時(shí)從配置好的 SecurityContextRepository 中獲取 SecurityContext,然后把它設(shè)置給SecurityContextHolder。在請求完成后將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時(shí)清除 securityContextHolder 所持有的 SecurityContext;
8.3.2?UsernamePasswordAuthenticationFilter?
? ??用于處理來自表單提交的認(rèn)證。該表單必須提供對應(yīng)的用戶名和密碼,其內(nèi)部還有登錄成功或失敗后進(jìn)行處理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,這些都可以根據(jù)需求做相關(guān)改變;
8.3.3?FilterSecurityInterceptor?
? ??是用于保護(hù)web資源的,使用AccessDecisionManager對當(dāng)前用戶進(jìn)行授權(quán)訪問;
8.3.4?ExceptionTranslationFilter?
? ??能夠捕獲來自 FilterChain 所有的異常,并進(jìn)行處理。但是它只會(huì)處理兩類異常:AuthenticationException 和 AccessDeniedException,其它的異常它會(huì)繼續(xù)拋出。
9.?【源碼分析】Spring Security認(rèn)證工作流程【重點(diǎn)】
SecurityContextPersistenceFilter?
UsernamePasswordAuthenticationFilter (attemptAuthentication)
ProviderManager(authenticate)
DaoAuthenticationProvider (retrieveUser)
AbstractUserDetailsAuthenticationProvider(authenticate)
?
9.1?認(rèn)證流程圖
?

9.2?流程圖分析
1.?用戶提交用戶名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器獲取到,封裝為請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個(gè)實(shí)現(xiàn)類。
2.?然后過濾器將Authentication提交至認(rèn)證管理器(AuthenticationManager)進(jìn)行認(rèn)證
3.?認(rèn)證成功后, AuthenticationManager 身份管理器返回一個(gè)被填充滿了信息的(包括上面提到的權(quán)限信息,身份信息,細(xì)節(jié)信息,但密碼通常會(huì)被移除) Authentication 實(shí)例。
4.?SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設(shè)置到其中??梢钥闯鯝uthenticationManager接口(認(rèn)證管理器)是認(rèn)證相關(guān)的核心接口,也是發(fā)起認(rèn)證的出發(fā)點(diǎn),它的實(shí)現(xiàn)類為ProviderManager。而Spring Security支持多種認(rèn)證方式,因此ProviderManager維護(hù)著一個(gè)List<AuthenticationProvider> 列表,存放多種認(rèn)證方式,最終實(shí)際的認(rèn)證工作是由AuthenticationProvider完成的。咱們知道web表單的對應(yīng)的AuthenticationProvider實(shí)現(xiàn)類為DaoAuthenticationProvider,它的內(nèi)部又維護(hù)著一個(gè)UserDetailsService負(fù)責(zé)UserDetails的獲取。最終AuthenticationProvider將UserDetails填充至Authentication
9.3?斷點(diǎn)調(diào)試及源碼分析
看上圖打斷點(diǎn)調(diào)試
9.4?結(jié)果總結(jié)
9.4.1?AuthenticationProvider
通過前面的Spring Security認(rèn)證流程我們得知,認(rèn)證管理器(AuthenticationManager)委托AuthenticationProvider完成認(rèn)證工作。
AuthenticationProvider是一個(gè)接口,定義如下:
authenticate()方法定義了認(rèn)證的實(shí)現(xiàn)過程,它的參數(shù)是一個(gè)Authentication,里面包含了登錄用戶所提交的用戶、密碼等。而返回值也是一個(gè)Authentication,這個(gè)Authentication則是在認(rèn)證成功后,將用戶的權(quán)限及其他信息重新組裝后生成。
Spring Security中維護(hù)著一個(gè) List<AuthenticationProvider> 列表,存放多種認(rèn)證方式,不同的認(rèn)證方式使用不同的AuthenticationProvider。如使用用戶名密碼登錄時(shí),使用AuthenticationProvider1,短信登錄時(shí)使用AuthenticationProvider2等等這樣的例子很多。
每個(gè)AuthenticationProvider需要實(shí)現(xiàn)supports()方法來表明自己支持的認(rèn)證方式,如我們使用表單方式認(rèn)證,在提交請求時(shí)Spring Security會(huì)生成UsernamePasswordAuthenticationToken,它是一個(gè)Authentication,里面封裝著用戶提交的用戶名、密碼信息。而對應(yīng)的,哪個(gè)AuthenticationProvider來處理它?
我們在DaoAuthenticationProvider的基類AbstractUserDetailsAuthenticationProvider發(fā)現(xiàn)以下代碼:
public boolean supports(Class<?> authentication) {
????return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
也就是說當(dāng)web表單提交用戶名密碼時(shí),Spring Security由DaoAuthenticationProvider處理。
最后,我們來看一下 Authentication(認(rèn)證信息)的結(jié)構(gòu),它是一個(gè)接口,我們之前提到的
UsernamePasswordAuthenticationToken就是它的實(shí)現(xiàn)之一:
?public interface Authentication extends Principal, Serializable { ????????
????Collection<? extends GrantedAuthority> getAuthorities(); ?????????????
????Object getCredentials(); ????????????????????????????????????????????????????
????Object getDetails(); ?????????????????????????????????????????????????
????Object getPrincipal(); ???????????????????????????????????????????????
????boolean isAuthenticated(); ??????????????????????????????????????????
????void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
(1)Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位于 java.security
包中的。它是表示著一個(gè)抽象主體身份,任何主體都有一個(gè)名稱,因此包含一個(gè)getName()方法。
(2)getAuthorities(),權(quán)限信息列表,默認(rèn)是GrantedAuthority接口的一些實(shí)現(xiàn)類,通常是代表權(quán)限信息的一系
列字符串。
(3)getCredentials(),憑證信息,用戶輸入的密碼字符串,在認(rèn)證過后通常會(huì)被移除,用于保障安全。
(4)getDetails(),細(xì)節(jié)信息,web應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails,它記錄了訪問者的ip地
址和sessionId的值。
(5)getPrincipal(),身份信息,大部分情況下返回的是UserDetails接口的實(shí)現(xiàn)類,UserDetails代表用戶的詳細(xì)
信息,那從Authentication中取出來的UserDetails就是當(dāng)前登錄用戶信息,它也是框架中的常用接口之一。
9.4.2?UserDetailsService【重點(diǎn)】[自定義查詢數(shù)據(jù)庫]
9.4.2.1?認(rèn)識UserDetailsService
現(xiàn)在咱們現(xiàn)在知道DaoAuthenticationProvider處理了web表單的認(rèn)證邏輯,認(rèn)證成功后既得到一個(gè)Authentication(UsernamePasswordAuthenticationToken實(shí)現(xiàn)),里面包含了身份信息(Principal)。這個(gè)身份信息就是一個(gè) Object ,大多數(shù)情況下它可以被強(qiáng)轉(zhuǎn)為UserDetails對象。
DaoAuthenticationProvider中包含了一個(gè)UserDetailsService實(shí)例,它負(fù)責(zé)根據(jù)用戶名提取用戶信息UserDetails(包含密碼),而后DaoAuthenticationProvider會(huì)去對比UserDetailsService提取的用戶密碼與用戶提交的密碼是否匹配作為認(rèn)證成功的關(guān)鍵依據(jù),因此可以通過將自定義的 UserDetailsService 公開為spring bean來定義自定義身份驗(yàn)證。
public interface UserDetailsService { ??
????UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; ???
}
很多人把 DaoAuthenticationProvider和UserDetailsService的職責(zé)搞混淆,其實(shí)UserDetailsService只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫)加載用戶信息,僅此而已。而DaoAuthenticationProvider的職責(zé)更大,它完成完整的認(rèn)證流程,同時(shí)會(huì)把UserDetails填充至Authentication。
上面一直提到UserDetails是用戶信息,咱們看一下它的真面目:
它和Authentication接口很類似,比如它們都擁有username,authorities。Authentication的getCredentials()與UserDetails中的getPassword()需要被區(qū)分對待,前者是用戶提交的密碼憑證,后者是用戶實(shí)際存儲的密碼,認(rèn)證其實(shí)就是對這兩者的比對。Authentication中的getAuthorities()實(shí)際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication接口中的getDetails()方法嗎?其中的UserDetails用戶詳細(xì)信息便是經(jīng)過了AuthenticationProvider認(rèn)證之后被填充的。
通過實(shí)現(xiàn)UserDetailsService和UserDetails,我們可以完成對用戶信息獲取方式以及用戶信息字段的擴(kuò)展。
Spring Security提供的InMemoryUserDetailsManager(內(nèi)存認(rèn)證),JdbcUserDetailsManager(jdbc認(rèn)證)就是
UserDetailsService的實(shí)現(xiàn)類,主要區(qū)別無非就是從內(nèi)存還是從數(shù)據(jù)庫加載用戶。
9.4.2.2?測試
自定義UserDetailsService
重啟工程,請求認(rèn)證,SpringDataUserDetailsService的loadUserByUsername方法被調(diào)用 ,查詢用戶信息。
10.?【源碼分析】Spring Security授權(quán)工作流程[了解]
10.1?授權(quán)流程圖
通過快速上手我們知道,Spring Security可以通過 http.authorizeRequests() 對web請求進(jìn)行授權(quán)保護(hù)。SpringSecurity使用標(biāo)準(zhǔn)Filter建立了對web請求的攔截,最終實(shí)現(xiàn)對資源的授權(quán)訪問。
Spring Security的授權(quán)流程如下:
?

10.2?授權(quán)流程分析
10.2.1?攔截請求
已認(rèn)證用戶訪問受保護(hù)的web資源將被SecurityFilterChain中的 FilterSecurityInterceptor 的子類攔截。
10.2.2?獲取資源訪問策略
FilterSecurityInterceptor會(huì)從 SecurityMetadataSource 的子類DefaultFilterInvocationSecurityMetadataSource 獲取要訪問當(dāng)前資源所需要的權(quán)限Collection<ConfigAttribute> 。
SecurityMetadataSource其實(shí)就是讀取訪問策略的抽象,而讀取的內(nèi)容,其實(shí)就是我們配置的訪問規(guī)則, 讀取訪問策略如:
?

10.2.3?最后
FilterSecurityInterceptor會(huì)調(diào)用 AccessDecisionManager 進(jìn)行授權(quán)決策,若決策通過,則允許訪問資源,否則將禁止訪問。
AccessDecisionManager(訪問決策管理器)的核心接口如下:
這里著重說明一下decide的參數(shù):
authentication:要訪問資源的訪問者的身份
object:要訪問的受保護(hù)資源,web請求對應(yīng)FilterInvocation
configAttributes:是受保護(hù)資源的訪問策略,通過SecurityMetadataSource獲取。
decide接口就是用來鑒定當(dāng)前用戶是否有訪問對應(yīng)受保護(hù)資源的權(quán)限。
10.3?授權(quán)決策分析
AccessDecisionManager采用投票的方式來確定是否能夠訪問受保護(hù)資源。
AccessDecisionManager中包含的一系列AccessDecisionVoter將會(huì)被用來對Authentication是否有權(quán)訪問受保護(hù)對象進(jìn)行投票,AccessDecisionManager根據(jù)投票結(jié)果,做出最終決策。
AccessDecisionVoter是一個(gè)接口,其中定義有三個(gè)方法,具體結(jié)構(gòu)如下所示。
public interface AccessDecisionVoter<S> {
????int ACCESS_GRANTED = 1;
????int ACCESS_ABSTAIN = 0;
????int ACCESS_DENIED = ‐1;
????boolean supports(ConfigAttribute var1);
????boolean supports(Class<?> var1);
????int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
vote()方法的返回結(jié)果會(huì)是AccessDecisionVoter中定義的三個(gè)常量之一。ACCESS_GRANTED表示同意,ACCESS_DENIED表示拒絕,ACCESS_ABSTAIN表示棄權(quán)。如果一個(gè)AccessDecisionVoter不能判定當(dāng)前Authentication是否擁有訪問對應(yīng)受保護(hù)對象的權(quán)限,則其vote()方法的返回值應(yīng)當(dāng)為棄權(quán)ACCESS_ABSTAIN。
? ? ? Spring Security內(nèi)置了三個(gè)基于投票的AccessDecisionManager實(shí)現(xiàn)類如下,它們分別是AffirmativeBased、ConsensusBased和UnanimousBased,。
10.3.1?AffirmativeBased的邏輯是:(一票通過)
?(1)只要有AccessDecisionVoter的投票為ACCESS_GRANTED則同意用戶進(jìn)行訪問;
?(2)如果全部棄權(quán)也表示通過;
?(3)如果沒有一個(gè)人投贊成票,但是有人投反對票,則將拋出AccessDeniedException。
Spring security默認(rèn)使用的是AffirmativeBased。
10.3.2?ConsensusBased的邏輯是:(多數(shù)派)
(1)如果贊成票多于反對票則表示通過。
(2)反過來,如果反對票多于贊成票則將拋出AccessDeniedException。
(3)如果贊成票與反對票相同且不等于0,并且屬性allowIfEqualGrantedDeniedDecisions的值為true,則表示通過,否則將拋出異常AccessDeniedException。參數(shù)allowIfEqualGrantedDeniedDecisions的值默認(rèn)為true。
(4)如果所有的AccessDecisionVoter都棄權(quán)了,則將視參數(shù)allowIfAllAbstainDecisions的值而定,如果該值為true則表示通過,否則將拋出異常AccessDeniedException。參數(shù)allowIfAllAbstainDecisions的值默認(rèn)為false。
10.3.3?UnanimousBased的邏輯具體是:
UnanimousBased的邏輯與另外兩種實(shí)現(xiàn)有點(diǎn)不一樣,另外兩種會(huì)一次性把受保護(hù)對象的配置屬性全部傳遞給AccessDecisionVoter進(jìn)行投票,而UnanimousBased會(huì)一次只傳遞一個(gè)ConfigAttribute給AccessDecisionVoter進(jìn)行投票。這也就意味著如果我們的AccessDecisionVoter的邏輯是只要傳遞進(jìn)來的
ConfigAttribute中有一個(gè)能夠匹配則投贊成票,但是放到UnanimousBased中其投票結(jié)果就不一定是贊成了。
UnanimousBased的邏輯具體來說是這樣的:
(1)如果受保護(hù)對象配置的某一個(gè)ConfigAttribute被任意的AccessDecisionVoter反對了,則將拋出AccessDeniedException。
(2)如果沒有反對票,但是有贊成票,則表示通過。
(3)如果全部棄權(quán)了,則將視參數(shù)allowIfAllAbstainDecisions的值而定,true則通過,false則拋出AccessDeniedException。
Spring Security也內(nèi)置一些投票者實(shí)現(xiàn)類如RoleVoter、AuthenticatedVoter和WebExpressionVoter等,可以自行查閱資料進(jìn)行學(xué)習(xí)。
?
總結(jié):認(rèn)證和鑒權(quán)都是過濾器鏈 ,認(rèn)證是重點(diǎn)關(guān)注
UserDetailsService接口 loadUserByUsername() 我們可以實(shí)現(xiàn)這個(gè)接口 集成數(shù)據(jù)庫
11.?Spring Security集成Thymeleaf詳解
Springsecurity+mysql完成認(rèn)證和授權(quán)
1,自定義訪問數(shù)據(jù)庫UserDetailsService接口 loadUserByUsername()
2,自定義登陸頁面
3,能不能自定義登陸的請求地址呢 /默認(rèn)為/login
4,能不能自定義登出的地址呢 ?默認(rèn)為/logout
5,能不能自定義表單的名字 ?默認(rèn)為username password
?
11.1?準(zhǔn)備數(shù)據(jù)庫
?

11.2?創(chuàng)建新項(xiàng)目選擇依賴
?

11.3?pom.xml
11.4?application.yml
11.5?修改啟動(dòng)類
11.6?逆向生成SysUser
?

11.7?修改SysUser【重點(diǎn)】
因?yàn)槲覀円咦远x登錄方法,方法需要返回UserDetails,所以我們就這么來
11.8?創(chuàng)建AppUserDetailsServiceImpl【重點(diǎn)】
11.9?修改SysUserMapper
11.10?修改SysUserMapper.xml文件(sql)
11.11?創(chuàng)建WebSecurityConfig配置類【重點(diǎn)】
11.12?創(chuàng)建RouterController
11.13?創(chuàng)建UserController
11.14?創(chuàng)建頁面
11.14.1?在tempaltes下面創(chuàng)建main.html和login.html
創(chuàng)建main.html
創(chuàng)建login.html
11.14.2?在tempaltes/user下面創(chuàng)建頁面
創(chuàng)建export.html
創(chuàng)建query.html
創(chuàng)建add.html
創(chuàng)建update.html
?
11.14.3?在static/error下面創(chuàng)建頁面
創(chuàng)建403.html
11.15?啟動(dòng)測試即可
11.16?當(dāng)用戶沒有某權(quán)限時(shí),頁面不展示該按鈕
上一講里面我們創(chuàng)建的項(xiàng)目里面是當(dāng)用戶點(diǎn)擊頁面上的鏈接請求到后臺之后沒有權(quán)限會(huì)跳轉(zhuǎn)到403,那么如果用戶沒有權(quán)限,對應(yīng)的按鈕就不顯示出來,這樣豈不是更好嗎
我們接著上一個(gè)項(xiàng)目來改造
引入下面的依賴
?
11.17?登錄后查看效果
?

12.?Spring Security集成Thymeleaf圖片驗(yàn)證碼
以前因?yàn)槲覀冏约簩懙顷懙姆椒梢栽谧约旱牡顷懛椒ɡ锩嫒ソ邮枕撁鎮(zhèn)鬟^來的code再和session里面正確的code進(jìn)行比較 ??
12.1?概述
上一講里面我們集成了Thymeleaf實(shí)現(xiàn)在頁面鏈接的動(dòng)態(tài)判斷是否顯示,那么在實(shí)際開發(fā)中,我們會(huì)遇到有驗(yàn)證碼的功能,那么如何處理呢?
我們接著上一個(gè)項(xiàng)目來改造
12.2?原理、存在問題、解決思路
我們知道Spring Security是通過過濾器鏈來完成了,所以它的解決方案是創(chuàng)建一個(gè)過濾器放到Security的過濾器鏈中,在自定義的過濾器中比較驗(yàn)證碼
12.3?添加依賴(生成驗(yàn)證碼)
12.4?添加一個(gè)獲取驗(yàn)證碼的接口
12.5?創(chuàng)建驗(yàn)證碼過濾器ValidateCodeFilter【重點(diǎn)】
12.6?修改WebSecurityConfig【重點(diǎn)】
?
12.7?修改login.html
12.8?測試登錄即可
故意填錯(cuò)驗(yàn)證碼
?

13.?Spring Security集成Thymeleaf+ajax的驗(yàn)證碼處理
13.1?創(chuàng)建項(xiàng)目
繼續(xù)上一個(gè)項(xiàng)目的修改
13.2?加入依賴
不用動(dòng)
13.3?創(chuàng)建Result
13.4?創(chuàng)建四個(gè)處理器
13.4.1?創(chuàng)建登陸成功的處理器AppAuthenticationSuccessHandler
?
13.4.2?創(chuàng)建登陸失敗處理器AppAuthenticationFailureHandler
?
?
13.4.3?創(chuàng)建無權(quán)限處理器AppAccessDeniedHandler
?
13.4.4?創(chuàng)建登出處理器AppLogoutSuccessHandler
?
?
13.5?修改ValidateCodeFilter處理驗(yàn)證碼
?
13.6?創(chuàng)建WebSecurityConfig配置
?
13.7?引入vue代碼和axios
?

13.8?修改templates/login.html
13.9?啟動(dòng)測試
?

14.?【掌握】JWT概述
14.1?概述
14.1.1?什么是JWT
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519).該token被設(shè)計(jì)為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密【可解析的】。
官網(wǎng): https://jwt.io/?
14.1.2?跨域認(rèn)證問題
互聯(lián)網(wǎng)服務(wù)離不開用戶認(rèn)證。一般流程是下面這樣。
l?用戶向服務(wù)器發(fā)送用戶名和密碼。
l?服務(wù)器驗(yàn)證通過后,在當(dāng)前對話(session)里面保存相關(guān)數(shù)據(jù),比如用戶角色、登錄時(shí)間等等。
l?服務(wù)器向用戶返回一個(gè) jsession_id,寫入用戶的 Cookie。
l?用戶隨后的每一次請求,都會(huì)通過 Cookie,將 session_id 傳回服務(wù)器。
l?服務(wù)器收到 session_id,找到前期保存的數(shù)據(jù),由此得知用戶的身份。
這種模式的問題在于,擴(kuò)展性(scaling)不好。單機(jī)當(dāng)然沒有問題,如果是服務(wù)器集群,或者是跨域的服務(wù)導(dǎo)向架構(gòu),就要求 session 數(shù)據(jù)共享,每臺服務(wù)器都能夠讀取 session。
舉例來說,A 網(wǎng)站和 B 網(wǎng)站是同一家公司的關(guān)聯(lián)服務(wù)?,F(xiàn)在要求,用戶只要在其中一個(gè)網(wǎng)站登錄,再訪問另一個(gè)網(wǎng)站就會(huì)自動(dòng)登錄,請問怎么實(shí)現(xiàn)?
一種解決方案是 session 數(shù)據(jù)持久化,寫入數(shù)據(jù)庫或別的持久層。各種服務(wù)收到請求后,都向持久層請求數(shù)據(jù)。這種方案的優(yōu)點(diǎn)是架構(gòu)清晰,缺點(diǎn)是工程量比較大。另外,持久層萬一掛了,就會(huì)單點(diǎn)失敗。
另一種方案是服務(wù)器索性不保存 session 數(shù)據(jù)了,所有數(shù)據(jù)都保存在客戶端,每次請求都發(fā)回服務(wù)器。JWT 就是這種方案的一個(gè)代表。?服務(wù)器不存數(shù)據(jù),客戶端存,服務(wù)器解析就行了
14.2?JWT 的原理
JWT 的原理是,服務(wù)器認(rèn)證成功以后,生成一個(gè) JSON 對象,發(fā)回給用戶,就像下面這樣。
{
??"姓名": "張三",
??"角色": "管理員",
??"到期時(shí)間": "2018年7月1日0點(diǎn)0分"
}
以后,用戶與服務(wù)端通信的時(shí)候,都要發(fā)回這個(gè) JSON 對象。服務(wù)器完全只靠這個(gè)對象認(rèn)定用戶身份。為了防止用戶篡改數(shù)據(jù),服務(wù)器在生成這個(gè)對象的時(shí)候,會(huì)加上簽名(詳見后文)。
服務(wù)器就不保存任何 session 數(shù)據(jù)了,也就是說,服務(wù)器變成無狀態(tài)了,從而比較容易實(shí)現(xiàn)擴(kuò)展。
14.3?JWT 的數(shù)據(jù)結(jié)構(gòu)
實(shí)際的 JWT 大概就像下面這樣。
?

它是一個(gè)很長的字符串,中間用點(diǎn)(.)分隔成三個(gè)部分。注意,JWT 內(nèi)部是沒有換行的,這里只是為了便于展示,將它寫成了幾行。
JWT 的三個(gè)部分依次如下。?
面試問題: jwt知道嗎?談?wù)勀愕睦斫猓ㄓ蓽\入深的聊)
??Header(頭部)
??Payload(負(fù)載)
??Signature(簽名)
寫成一行,就是下面的樣子。
Header.Payload.Signature
?
?

下面依次介紹這三個(gè)部分。
14.3.1?Header
Header 部分是一個(gè) JSON 對象,描述 JWT 的元數(shù)據(jù),通常是下面的樣子。
{
??"alg": "HS256",
??"typ": "JWT"
}
上面代碼中,alg屬性表示簽名的算法(algorithm),默認(rèn)是 HMAC SHA256(寫成 HS256);typ屬性表示這個(gè)令牌(token)的類型(type),JWT 令牌統(tǒng)一寫為JWT。
最后,將上面的 JSON 對象使用?Base64URL 算法轉(zhuǎn)成字符串。
14.3.2?Payload
Payload 部分也是一個(gè) JSON 對象,用來存放實(shí)際需要傳遞的數(shù)據(jù)。JWT 規(guī)定了7個(gè)官方字段,供選用。
iss (issuer):簽發(fā)人
exp (expiration time):過期時(shí)間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時(shí)間
iat (Issued At):簽發(fā)時(shí)間
jti (JWT ID):編號
除了官方字段,你還可以在這個(gè)部分定義私有字段,下面就是一個(gè)例子。
{
??"sub": "1234567890",
??"name": "John Doe",
??"admin": true
}
注意,JWT 默認(rèn)是不加密的,任何人都可以讀到,所以不要把秘密信息(密碼,手機(jī)號等)放在這個(gè)部分。
這個(gè) JSON 對象也要使用 Base64URL 算法轉(zhuǎn)成字符串。
14.3.3?Signature(保證數(shù)據(jù)安全性的)
Signature 部分是對前兩部分的簽名,防止數(shù)據(jù)篡改。
首先,需要指定一個(gè)密鑰(secret)。這個(gè)密鑰只有服務(wù)器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認(rèn)是 HMAC SHA256),按照下面的公式產(chǎn)生簽名。
?HMACSHA256(
??base64UrlEncode(header) + "." +
??base64UrlEncode(payload),
??secret)
算出簽名以后,把 Header、Payload、Signature 三個(gè)部分拼成一個(gè)字符串,每個(gè)部分之間用"點(diǎn)"(.)分隔,就可以返回給用戶。
14.3.4?Base64URL(轉(zhuǎn)碼)
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個(gè)算法跟 Base64 算法基本類似,但有一些小的不同。
JWT 作為一個(gè)令牌(token),有些場合可能會(huì)放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個(gè)字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
14.4?JWT 的使用方式【重點(diǎn)】
客戶端收到服務(wù)器返回的 JWT,可以儲存在 Cookie 里面,也可以儲存在 localStorage。
此后,客戶端每次與服務(wù)器通信,都要帶上這個(gè) JWT。你可以把它放在 Cookie 里面自動(dòng)發(fā)送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭信息Authorization字段里面。
Authorization: Bearer?jwt
另一種做法是,跨域的時(shí)候,JWT 就放在 POST 請求的數(shù)據(jù)體里面。
14.5?JWT 的幾個(gè)特點(diǎn)
JWT 默認(rèn)是不加密,但也是可以加密的。生成原始 Token 以后,可以用密鑰再加密一次。
JWT 不加密的情況下,不能將秘密數(shù)據(jù)寫入 JWT。
JWT 不僅可以用于認(rèn)證,也可以用于交換信息。有效使用 JWT,可以降低服務(wù)器查詢數(shù)據(jù)庫的次數(shù)。
JWT 的最大缺點(diǎn)是,由于服務(wù)器不保存 session 狀態(tài),因此無法在使用過程中廢止某個(gè) token,或者更改 token 的權(quán)限。也就是說,一旦 JWT 簽發(fā)了,在到期之前就會(huì)始終有效,除非服務(wù)器部署額外的邏輯(JWT的登出問題)。就是因?yàn)榉?wù)端無狀態(tài)了
正常情況下 修改了密碼后就會(huì)跳轉(zhuǎn)到登錄頁面 :修改成功后清空瀏覽器保存的token了
后端怎么玩? 因?yàn)榉?wù)端不保留token 我用之前的token 還是可以繼續(xù)訪問的
從有狀態(tài)(后端也會(huì)存一個(gè))的變成無狀態(tài)的了
我們就要把它從無狀態(tài)再變成有狀態(tài)了
JWT 本身包含了認(rèn)證信息,一旦泄露,任何人都可以獲得該令牌的所有權(quán)限。為了減少盜用,JWT 的有效期應(yīng)該設(shè)置得比較短。對于一些比較重要的權(quán)限,使用時(shí)應(yīng)該再次對用戶進(jìn)行認(rèn)證。
為了減少盜用,JWT 不應(yīng)該使用 HTTP?80?協(xié)議明碼傳輸,要使用 HTTPS?443?協(xié)議傳輸。
我們頒發(fā)一個(gè)令牌 ?用戶名稱 用戶的權(quán)限信息 ??這個(gè)令牌2個(gè)小時(shí)有效
Jwt只要能解析 就認(rèn)為你是可用的 ??做不了 登出 ?后端不存儲用戶信息了 后端無狀態(tài)了

由于字?jǐn)?shù)限制本文只分享了一半哦~
完整版獲取可私信小動(dòng)~
更多干貨我們下期再說!
下期會(huì)分享
第十九章節(jié)
RocketMQ
相關(guān)知識~
下期見!

教程揭秘 | 動(dòng)力節(jié)點(diǎn)內(nèi)部Java零基礎(chǔ)教學(xué)文檔第十八篇:SpringSecurity的評論 (共 條)
