本專欄將從基礎(chǔ)開始,循序漸進,以實戰(zhàn)為線索,逐步深入SpringSecurity相關(guān)知識相關(guān)知識,打造完整的SpringSecurity學(xué)習(xí)步驟,提升工程化編碼能力和思維能力,寫出高質(zhì)量代碼。希望大家都能夠從中有所收獲,也請大家多多支持。
專欄地址:SpringSecurity專欄
本文涉及的代碼都已放在gitee上:gitee地址
如果文章知識點有錯誤的地方,請指正!大家一起學(xué)習(xí),一起進步。
專欄匯總:專欄匯總
本章涵蓋了
- 用UserDetails接口描述一個用戶
- 在認證流程中使用UserDetailsService
- 創(chuàng)建一個自定義的UserDetailsService的實現(xiàn)
- 創(chuàng)建UserDetailsManager的自定義實現(xiàn)?在認證流程中使用JdbcUserDetailsManager
我的一位大學(xué)同事的廚藝很好。他不是高級餐廳的廚師,但他對烹飪相當有熱情。有一天,在討論中分享想法時,我問他如何設(shè)法記住這么多食譜。他告訴我,這很容易?!澳悴槐赜涀≌麄€食譜,但要記住基本成分之間的搭配方式。這就像一些現(xiàn)實世界的合約,告訴你什么可以混合或不應(yīng)該混合。然后對于每個配方,你只記得一些技巧”。
這個比喻類似于架構(gòu)的工作方式。對于任何強大的框架,我們都會使用契約來將框架的實現(xiàn)與建立在其上的應(yīng)用解耦。在Java中,我們使用接口來定義合同。程序員類似于廚師,知道各種成分是如何 "運作 "的,從而選擇合適的 “實現(xiàn)”。程序員知道框架的抽象,并使用這些抽象來與之整合。
本章是關(guān)于詳細了解你在第2章的第一個例子中遇到的基本角色之一–UserDetailsService。與UserDetailsService一起,我們將討論:
- UserDetails,它為Spring Security描述用戶。
- GrantedAuthority,它允許我們定義用戶可以執(zhí)行的動作。
- UserDetailsManager,它擴展了UserDetailsService合約。除了繼承的行為,它還描述了創(chuàng)建用戶和修改或刪除用戶密碼等動作。
通過第二章,你已經(jīng)對UserDetailsService和PasswordEncoder在認證過程中的作用有了一個概念。但我們只討論了如何插入一個由你定義的實例,而不是使用Spring Boot配置的默認實例。我們還有更多細節(jié)要討論:
- Spring Security提供了哪些實現(xiàn)以及如何使用它們
- 如何為合同定義一個自定義的實現(xiàn),以及何時這樣做
- 實現(xiàn)你在現(xiàn)實世界應(yīng)用中發(fā)現(xiàn)的接口的方法
- 使用這些接口的最佳實踐
計劃從Spring Security如何理解用戶定義開始。為此,我們將討論UserDetails和GrantedAuthority合約。然后,我們將詳細介紹UserDetailsService以及UserDetailsManager如何擴展這個契約。你將應(yīng)用這些接口的實現(xiàn)(比如InMemoryUserDetailsManager,JdbcUserDetailsManager,以及LdapUserDetailsManager)。當這些實現(xiàn)不適合你的系統(tǒng)時,你會寫一個自定義實現(xiàn)。
3.1 在Spring Security中實現(xiàn)認證
在上一章中,我們開始了Spring Security的學(xué)習(xí)。在第一個例子中,我們討論了Spring Boot是如何定義一些默認值的,這些默認值定義了一個新的應(yīng)用程序最初的工作方式。你還學(xué)習(xí)了如何使用我們經(jīng)常在應(yīng)用程序中發(fā)現(xiàn)的各種替代方法來覆蓋這些默認值。但我們只考慮了這些的表面情況,以便你對我們要做的事情有一個概念。在這一章,以及第四章和第五章中,我們將更詳細地討論這些接口,以及不同的實現(xiàn)和你可能在現(xiàn)實世界的應(yīng)用中找到它們。
圖3.1展示了Spring Security中的認證流程。這個架構(gòu)是Spring Security實現(xiàn)的認證過程的骨干。了解它真的很重要,因為你將在任何Spring Security的實現(xiàn)中依賴它。你會發(fā)現(xiàn),我們幾乎在本書的所有章節(jié)中都討論了這個架構(gòu)的一部分。你會經(jīng)常看到它,以至于你可能會把它背下來,這很好。如果你知道這個架構(gòu),你就像一個知道自己的成分的廚師,可以把任何食譜放在一起。
在圖3.1中,陰影框代表我們開始使用的組件:UserDetailsService和PasswordEncoder。這兩個組件集中在流程的一部分,我經(jīng)常把它稱為 “用戶管理部分”。在本章中,UserDetailsService和PasswordEncoder是直接處理用戶細節(jié)和他們的證書的組件。我們將在第四章詳細討論PasswordEncoder。我還將在本書中詳細介紹你可以在認證流程中定制的其他組件:在第5章中,我們將看看AuthenticationProvider和SecurityContext,在第9章中,我們將看看過濾器。
圖3.1 Spring Security的認證流程。AuthenticationFilter攔截請求并將認證責(zé)任委托給AuthenticationManager。為了實現(xiàn)認證邏輯,AuthenticationManager使用一個認證提供者。為了檢查用戶名和密碼,AuthenticationProvider使用UserDetailsService和PasswordEncoder。
作為用戶管理的一部分,我們使用UserDetailsService和UserDetailsManager接口。UserDetailsService只負責(zé)按用戶名檢索用戶。這個動作是框架完成認證所需要的唯一動作。UserDetailsManager增加了關(guān)于添加、修改或刪除用戶的行為,這在大多數(shù)應(yīng)用程序中都是必需的功能。這兩個契約之間的分離是接口隔離原則的一個很好的例子。分離接口可以獲得更好的靈活性,因為如果你的應(yīng)用程序不需要,框架不會強迫你實現(xiàn)行為。如果應(yīng)用程序只需要驗證用戶,那么實現(xiàn)UserDetailsService合同就足以涵蓋所需的功能。為了管理用戶,UserDetailsService和UserDetailsManager組件需要一種方法來表示它們。
Spring Security提供了UserDetails契約,你必須實現(xiàn)它來以框架理解的方式描述用戶。正如你在本章中所了解的,在Spring Security中,用戶有一組權(quán)限,也就是用戶被允許做的動作。我們將在第7章和第8章討論授權(quán)問題時,大量使用這些權(quán)限。但現(xiàn)在,Spring Security用GrantedAuthority接口表示用戶可以做的動作。我們通常稱這些權(quán)限,一個用戶有一個或多個權(quán)限。在圖3.2中,你可以看到認證流程中的用戶管理部分的組件之間的關(guān)系表示。
圖3.2 參與用戶管理的組件之間的依賴關(guān)系。UserDetailsService返回一個用戶的詳細信息,通過其名字找到用戶。UserDetails合約描述了用戶。一個用戶有一個或多個權(quán)限,由GrantedAuthority接口表示。為了給用戶添加諸如創(chuàng)建、刪除或更改密碼等操作,UserDetailsManager契約擴展了UserDetailsService來添加操作。
了解Spring Security架構(gòu)中這些對象之間的聯(lián)系以及實現(xiàn)它們的方法,可以讓你在處理應(yīng)用程序時有多種選擇。這些選項中的任何一個都可能是你正在開發(fā)的應(yīng)用程序中的正確拼圖,你需要明智地做出選擇。但為了能夠選擇,你首先需要知道你可以選擇什么。
3.2 描述用戶
在本節(jié)中,你將學(xué)習(xí)如何描述你的應(yīng)用程序的用戶,以便Spring Security能夠理解他們。學(xué)習(xí)如何表示用戶并使框架了解他們是構(gòu)建認證流程的一個重要步驟?;谟脩?,應(yīng)用程序會做出一個決定–對某一功能的調(diào)用是否被允許。為了與用戶打交道,你首先需要了解如何在你的應(yīng)用程序中定義用戶的原型。在這一節(jié)中,我將通過實例描述如何在Spring Security應(yīng)用程序中為用戶建立一個藍圖。
對于Spring Security來說,用戶定義應(yīng)該尊重UserDetails合約。UserDetails合約代表了Spring Security所理解的用戶。你的應(yīng)用程序中描述用戶的類必須實現(xiàn)這個接口,通過這種方式,框架可以理解它。
3.2.1 解讀UserDetails合同的定義
在本節(jié)中,你將學(xué)習(xí)如何實現(xiàn)UserDetails接口來描述你的應(yīng)用程序中的用戶。我們將討論UserDetails合約所聲明的方法,以了解我們?nèi)绾我约盀槭裁匆獙崿F(xiàn)每一個方法。首先,讓我們看看下面列表中介紹的接口。
清單3.1 UserDetails 接口
getUsername()和getPassword()方法返回,正如你所期望的,用戶名和密碼。應(yīng)用程序在認證過程中使用這些值,這些是該合同中唯一與認證有關(guān)的細節(jié)。其他五個方法都與授權(quán)用戶訪問應(yīng)用程序的資源有關(guān)。
一般來說,應(yīng)用程序應(yīng)該允許用戶做一些在應(yīng)用程序的上下文中有意義的動作。例如,用戶應(yīng)該能夠讀取數(shù)據(jù)、寫入數(shù)據(jù)或刪除數(shù)據(jù)。我們說一個用戶有或沒有執(zhí)行某個動作的權(quán)限,而一個權(quán)限代表一個用戶擁有的權(quán)限。我們實現(xiàn)getAuthorities()方法來返回授予用戶的權(quán)限組。
注意 正如你將在第7章中學(xué)習(xí)的那樣,Spring Security使用權(quán)限來指代細粒度的權(quán)限或角色,后者是權(quán)限的組。為了使你的閱讀更加輕松,在本書中,我把細粒度的權(quán)限稱為權(quán)限。
此外,正如在UserDetails合同中所看到的,用戶可以:
- 讓賬戶過期
- 鎖定賬戶
- 讓憑證過期
- 禁用該帳戶
如果你選擇在你的應(yīng)用程序的邏輯中實現(xiàn)這些用戶限制,你需要覆蓋以下方法:isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled(),使那些需要啟用的方法返回true。并非所有的應(yīng)用程序都有過期或在某些條件下被鎖定的賬戶。如果你不需要在你的應(yīng)用程序中實現(xiàn)這些功能,你可以簡單地讓這四個方法返回真。
注意 UserDetails接口中最后四個方法的名字可能聽起來很奇怪??梢哉f,從簡潔的編碼和可維護性的角度來看,這些方法的選擇是不明智的。例如,isAccountNonExpired()這個名字看起來像一個雙重否定,乍一看,可能會產(chǎn)生混淆。但是,要注意分析所有四個方法的名稱。這些方法的命名是這樣的:在授權(quán)失敗的情況下,它們都返回false,否則返回true。這是正確的方法,因為人類的思維傾向于將 "假 "字與消極性聯(lián)系起來,將 "真 "字與積極的情況聯(lián)系起來。
3.2.2 關(guān)于GrantedAuthority合同的詳細說明
正如你在第3.2.1節(jié)UserDetails接口的定義中所觀察到的,授予一個用戶的行動被稱為權(quán)限。在第7章和第8章中,我們將基于這些用戶權(quán)限來編寫授權(quán)配置。因此,知道如何定義它們是很有必要的。
授權(quán)代表了用戶在你的應(yīng)用程序中可以做什么。沒有權(quán)限,所有的用戶都是平等的。雖然有一些簡單的應(yīng)用程序中的用戶是平等的,但在大多數(shù)實際情況下,一個應(yīng)用程序會定義多種類型的用戶。一個應(yīng)用程序可能有只能閱讀特定信息的用戶,而其他人也可以修改數(shù)據(jù)。而你需要根據(jù)應(yīng)用的功能需求,使你的應(yīng)用對他們進行區(qū)分,這就是用戶需要的權(quán)限。為了描述Spring Security中的權(quán)限,你可以使用GrantedAuthority接口。
在我們討論實現(xiàn)UserDetails之前,讓我們先了解一下GrantedAuthority接口。我們在定義用戶詳細信息時使用這個接口。它代表了授予用戶的特權(quán)。一個用戶可以沒有任何數(shù)量的權(quán)限,通常,他們至少有一個。下面是GrantedAuthority定義的實現(xiàn)。
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
要創(chuàng)建一個權(quán)限,你只需要為該權(quán)限找到一個名稱,這樣你就可以在以后編寫授權(quán)規(guī)則時參考它。例如,一個用戶可以讀取應(yīng)用程序所管理的記錄或刪除它們。你可以根據(jù)你給這些動作起的名字來編寫授權(quán)規(guī)則。在第7章和第8章,你將學(xué)習(xí)如何根據(jù)用戶的權(quán)限來編寫授權(quán)規(guī)則。
在本章中,我們將實現(xiàn)getAuthority()方法,以字符串形式返回權(quán)限名稱。GrantedAuthority接口只有一個抽象方法,在本書中,你經(jīng)常會發(fā)現(xiàn)一些例子,我們使用lambda表達式來實現(xiàn)它。另一種可能性是使用SimpleGranted- Authority類來創(chuàng)建權(quán)限實例。
SimpleGrantedAuthority類提供了一種方法來創(chuàng)建GrantedAuthority類型的不可變實例。你在建立實例時提供了權(quán)限名稱。在接下來的代碼片段中,你會發(fā)現(xiàn)兩個實現(xiàn)GrantedAuthority的例子。在這里,我們利用一個lambda表達式,然后使用SimpleGrantedAuthority類。
注意 在用lambda表達式實現(xiàn)接口之前,用@FunctionalInterface注解驗證該接口是否被標記為功能性的,這是一個好的做法。這種做法的原因是,如果接口沒有被標記為功能性,就意味著其開發(fā)者保留了在未來版本中為其添加更多抽象方法的權(quán)利。在Spring Security中,GrantedAuthority接口沒有被標記為功能性的。然而,我們將在本書中使用lambda表達式來實現(xiàn)該接口,以使代碼更短、更容易閱讀,即使這不是我推薦你在真實世界的項目中做的事情。
3.2.3 編寫UserDetails的最小實現(xiàn)
在這一節(jié)中,你將編寫UserDetails合約的第一個實現(xiàn)。我們從一個基本的實現(xiàn)開始,其中每個方法返回一個靜態(tài)值。然后我們把它改成一個你更有可能在實際場景中找到的版本,一個允許你有多個不同用戶實例的版本?,F(xiàn)在你知道了如何實現(xiàn)UserDetails和GrantedAuthority接口,我們可以為一個應(yīng)用程序編寫最簡單的用戶定義。
通過一個名為DummyUser的類,我們來實現(xiàn)列表3.2中對用戶的最小描述。我使用這個類主要是為了演示實現(xiàn)UserDetails契約的方法。這個類的實例總是只提到一個用戶,“bill”,他有一個密碼 "12345 "和一個名為 "READ "的權(quán)限。
清單3.2 DummyUser類
public class DummyUser implements UserDetails {
@Override
public String getUsername() {
return "bill";
}
@Override
public String getPassword() {
return "12345";
}
// Omitted code
}
列表3.2中的類實現(xiàn)了UserDetails接口,需要實現(xiàn)它的所有方法。你可以在這里找到 getUsername() 和 getPassword() 的實現(xiàn)。在這個例子中,這些方法只為每個屬性返回一個固定的值。
接下來,我們?yōu)闄?quán)限列表添加一個定義。清單3.3顯示了getAuthorities()方法的實現(xiàn)。這個方法返回一個只有一個GrantedAuthority接口實現(xiàn)的集合。
清單3.3 getAuthorities()方法的實現(xiàn)
public class DummyUser implements UserDetails {
// Omitted code
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "READ");
}
// Omitted code
}
最后,你必須為UserDetails接口的最后四個方法添加一個實現(xiàn)。對于DummyUser類,這些方法總是返回true,這意味著用戶永遠是活躍的、可用的。你可以在下面的列表中找到這些例子。
清單3.4 最后四個UserDetails接口方法的實現(xiàn)
public class DummyUser implements UserDetails {
// Omitted code
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// Omitted code
}
當然,這種最小的實現(xiàn)意味著該類的所有實例都代表同一個用戶。這是理解契約的一個良好開端,但不是你在實際應(yīng)用中會做的事情。對于一個真實的應(yīng)用,你應(yīng)該創(chuàng)建一個可以用來生成代表不同用戶的實例的類。在這種情況下,你的定義至少要把用戶名和密碼作為類中的屬性,如下面的列表所示。
清單3.5 一個更實用的UserDetails接口的實現(xiàn)
public class SimpleUser implements UserDetails {
private final String username;
private final String password;
public SimpleUser(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return this.password;
}
// Omitted code
}
3.2.4 使用構(gòu)建器來創(chuàng)建UserDetails類型的實例
有些應(yīng)用程序很簡單,不需要自定義實現(xiàn)User- Details接口。在這一節(jié)中,我們看一下如何使用Spring Security提供的構(gòu)建器類來創(chuàng)建簡單的用戶實例。你不用在你的應(yīng)用程序中再聲明一個類,而是用User builder類快速獲得一個代表你的用戶的實例。
org.springframework.security.core.userdetails包中的User類是構(gòu)建UserDetails類型實例的一種簡單方法。使用這個類,你可以創(chuàng)建UserDetails的不可變的實例。你需要至少提供一個用戶名和一個密碼,而且用戶名不應(yīng)該是一個空字符串。清單3.6演示了如何使用這個構(gòu)建器。以這種方式構(gòu)建用戶,你不需要有UserDetails契約的實現(xiàn)。
清單3.6 用用戶構(gòu)建器類構(gòu)建一個用戶
UserDetails u = User.withUsername("bill")
.password("12345")
.authorities("read", "write")
.accountExpired(false)
.disabled(true) .build();
以前面的列表為例,讓我們更深入地了解User構(gòu)建器類的結(jié)構(gòu)。User.withUsername(String username)方法返回一個嵌套在 User 類中的構(gòu)建器類 UserBuilder 的實例。另一種創(chuàng)建構(gòu)建器的方法是從另一個 UserDetails 的實例開始。在列表3.7中,第一行構(gòu)建了一個UserBuilder,從給定的字符串的用戶名開始。之后,我們演示了如何從一個已經(jīng)存在的 UserDetails 實例開始創(chuàng)建一個構(gòu)建器。
清單3.7 創(chuàng)建User.UserBuilder實例
//用他們的用戶名建立一個用戶。
User.UserBuilder builder1 = User.withUsername("bill");
UserDetails u1 = builder1
.password("12345")
.authorities("read", "write")
//密碼編碼器只是一個做編碼的函數(shù)。
.passwordEncoder(p -> encode(p))
.accountExpired(false)
.disabled(true)
//在構(gòu)建管道的末端,調(diào)用build()方法
.build();
//你也可以從一個現(xiàn)有的UserDetails實例建立一個用戶。
User.UserBuilder builder2 = User.withUserDetails(u);
UserDetails u2 = builder2.build();
你可以看到,通過清單 3.7 中定義的任何一個構(gòu)建器,你可以使用構(gòu)建器來獲得由 UserDetails 合同代表的用戶。在構(gòu)建管道的末端,你調(diào)用 build() 方法。如果你提供了密碼,它將應(yīng)用定義的函數(shù)對密碼進行編碼,構(gòu)建 UserDetails 的實例,并返回它。
注意 密碼編碼器與我們在第2章討論的bean不同。這個名字可能讓人困惑,但在這里我們只有一個函數(shù)<String, String>。這個函數(shù)的唯一職責(zé)是在給定的編碼中轉(zhuǎn)換一個密碼字。在下一節(jié)中,我們將詳細討論我們在第二章中使用的來自Spring Security的PasswordEncoder合約。
3.2.5 結(jié)合與用戶有關(guān)的多種責(zé)任
在上一節(jié)中,你學(xué)到了如何實現(xiàn)UserDetails接口。在現(xiàn)實世界的場景中,它往往更復(fù)雜。在大多數(shù)情況下,你會發(fā)現(xiàn)一個用戶與多個職責(zé)相關(guān)。而如果你把用戶存儲在數(shù)據(jù)庫中,然后在應(yīng)用程序中,你也需要一個類來表示持久化實體?;蛘?,如果你通過網(wǎng)絡(luò)服務(wù)從另一個系統(tǒng)檢索用戶,那么你可能需要一個數(shù)據(jù)傳輸對象來表示用戶實例。假設(shè)第一種情況很簡單,但也很典型,讓我們考慮一下,我們在一個SQL數(shù)據(jù)庫中有一個表,我們在其中存儲用戶。為了讓這個例子更簡短,我們只給每個用戶一個權(quán)限。下面的列表顯示了映射該表的實體類。
清單3.8 定義JPA用戶實體類
@Entity
public class User {
@Id
private Long id;
private String username;
private String password;
private String authority;
// Omitted getters and setters
}
如果你讓同一個類也為用戶實現(xiàn)Spring Security合約的 細節(jié),這個類就會變得更加復(fù)雜。你對下一個列表中的代碼有什么看法?看起來如何?從我的角度來看,它是一團糟。我會迷失在其中。
清單3.9 用戶類有兩個職責(zé)
該類包含JPA注釋、getters和setters,其中g(shù)etUsername()和getPassword()都覆蓋了UserDetails合同中的方法。它有一個返回字符串的getAuthority()方法,以及一個返回集合的getAuthorities()方法。getAuthority()方法只是類中的一個getter,而getAuthorities()實現(xiàn)了UserDetails接口中的方法。而在添加與其他實體的關(guān)系時,事情就變得更加復(fù)雜了。再說一遍,這段代碼一點也不友好!
我們怎樣才能把這段代碼寫得更干凈呢?前面的代碼例子的泥濘方面的根源是兩個責(zé)任的混合。雖然在應(yīng)用程序中確實需要這兩種職責(zé),但在這種情況下,沒有人說你必須把它們放在同一個類中。讓我們試著通過定義一個單獨的名為SecurityUser的類來分離這些職責(zé),該類裝飾User類。如清單3.10所示,SecurityUser類實現(xiàn)了UserDetails契約,并使用它將我們的用戶插入到Spring的安全架構(gòu)中。User類只剩下它的JPA實體責(zé)任。
清單3.10 將用戶類僅作為JPA實體來實現(xiàn)
@Entity
public class User {
@Id
private int id;
private String username;
private String password;
private String authority;
// Omitted getters and setters
}
列表3.10中的User類只剩下了它的JPA實體責(zé)任,因此,變得更加可讀。如果你閱讀這段代碼,你現(xiàn)在可以只關(guān)注與持久化有關(guān)的細節(jié) 與持久化相關(guān)的細節(jié),從Spring Security的角度來看,這些細節(jié)并不重要。在 下一個列表中,我們實現(xiàn)了SecurityUser類來包裝用戶實體。
清單3.11 SecurityUser類實現(xiàn)了UserDetails合同。
public class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user) {
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> user.getAuthority());
}
// Omitted code
}
正如你所看到的,我們使用SecurityUser類只是為了將系統(tǒng)中的用戶細節(jié)映射到Spring Security所理解的UserDetails契約中。為了表明SecurityUser在沒有用戶實體的情況下是沒有意義的,我們把這個字段變成了最終的。你必須通過構(gòu)造函數(shù)來提供用戶。SecurityUser類對User實體類進行了分級,并添加了與Spring Security合同相關(guān)的所需代碼,而沒有將代碼混入JPA實體,從而實現(xiàn)了多個不同的任務(wù)。
注意 你可以找到不同的方法來分離這兩項職責(zé)。我不想說我在本節(jié)中介紹的方法是最好的或唯一的。通常情況下,你選擇的實現(xiàn)類設(shè)計的方式在不同的情況下有很大的不同。但主要的想法是一樣的:避免混合責(zé)任,盡量寫出解耦的代碼,以提高應(yīng)用程序的可維護性。
3.3 指導(dǎo)Spring Security如何管理用戶
在上一節(jié)中,你實現(xiàn)了UserDetails契約來描述用戶,以便Spring Security能夠理解他們。但Spring Security是如何管理用戶的呢?在比較憑證時,他們是從哪里來的,以及你如何添加新的用戶或改變現(xiàn)有的用戶?在第2章中,你了解到框架定義了一個特定的組件,認證過程將用戶管理委托給它:UserDetailsService實例。我們甚至定義了一個UserDetailsService來覆蓋Spring Boot提供的默認實現(xiàn)。
在這一節(jié)中,我們嘗試了實現(xiàn)UserDetailsService類的各種方法。通過在我們的例子中實現(xiàn)UserDetailsService合約所描述的責(zé)任,你將了解用戶管理是如何工作的。之后,你會發(fā)現(xiàn)UserDetailsManager接口如何為UserDetailsService定義的契約增加更多的行為。在本節(jié)的最后,我們將使用Spring Security提供的UserDetailsManager接口的實現(xiàn)。我們將寫一個示例項目,其中我們將使用Spring Security提供的最著名的實現(xiàn)之一–JdbcUserDetailsManager。通過學(xué)習(xí),你將知道如何告訴Spring Security在哪里找到用戶,這在認證流程中是至關(guān)重要的。
3.3.1 了解UserDetailsService合同
在本節(jié)中,你將了解UserDetailsService接口的定義。在理解如何以及為什么要實現(xiàn)它之前,你必須首先理解契約。現(xiàn)在是時候詳細介紹UserDetailsService以及如何與這個組件的實現(xiàn)一起工作了。UserDetailsService接口只包含一個方法,如下所示。
認證的實現(xiàn)會調(diào)用loadUserByUsername(String username)方法來獲取具有給定用戶名的用戶的詳細信息(圖3.3)。當然,該用戶名被認為是唯一的。這個方法返回的用戶是UserDetails合約的一個實現(xiàn)。如果該用戶名不存在,該方法會拋出一個 UsernameNotFoundException。
圖3.3 AuthenticationProvider是實現(xiàn)認證邏輯的組件,它使用UserDetailsService來加載用戶的詳細信息。為了按用戶名查找用戶,它調(diào)用loadUserByUsername(String username)方法。
注意 UsernameNotFoundException是一個RuntimeException。UserDetailsService接口中的throws子句僅僅是為了記錄的目的。UsernameNotFoundException直接繼承自AuthenticationException類型,它是所有與認證過程相關(guān)的異常的父類。AuthenticationException進一步繼承了RuntimeException類。
3.3.2 實現(xiàn)UserDetailsService合同
在本節(jié)中,我們通過一個實際的例子來演示UserDetailsService的實現(xiàn)。你的應(yīng)用程序管理著關(guān)于證書和其他用戶方面的細節(jié)。這些信息可能存儲在數(shù)據(jù)庫中,也可能由你通過Web服務(wù)或其他方式訪問的另一個系統(tǒng)處理(圖3.3)。不管這在你的系統(tǒng)中是如何發(fā)生的,Spring Security需要你做的唯一一件事就是實現(xiàn)按用戶名檢索用戶。
在下一個例子中,我們寫一個UserDetailsService,它有一個內(nèi)存中的用戶列表。在第2章中,你使用了一個提供的實現(xiàn)來做同樣的事情,即InMemoryUserDetailsManager。因為你已經(jīng)熟悉了這個實現(xiàn)的工作方式,所以我選擇了一個類似的功能,但這次是由我們自己實現(xiàn)的。當我們創(chuàng)建UserDetails- Service類的實例時,我們提供一個用戶列表。你可以在項目sia-ch3-ex1中找到這個例子。在名為model的包中,我們定義了UserDetails,如下表所示。
清單3.12 UserDetails接口的實現(xiàn)
package com.laurentiuspilca.ssia.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
//用戶類是不可改變的。當你建立實例時,你給出了三個屬性的值,而這些值在之后不能被改變。
private final String username;
private final String password;
private final String authority;
//為了使例子簡單,一個用戶只有一個權(quán)限。
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
//返回一個只包含GrantedAuthority對象的列表,該對象的名稱是在你建立實例時提供的。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> authority);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
//該賬戶不會過期或被鎖定。
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
在名為services的包中,我們創(chuàng)建了一個名為InMemoryUserDetailsService的類。下面的列表顯示了我們?nèi)绾螌崿F(xiàn)這個類。
清單3.13 UserDetailsService接口的實現(xiàn)
package com.laurentiuspilca.ssia.services;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.List;
public class InMemoryUserDetailsService implements UserDetailsService {
//UserDetailsService在內(nèi)存中管理用戶的列表。
private final List<UserDetails> users;
public InMemoryUserDetailsService(List<UserDetails> users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.stream()
//從用戶列表中,篩選出具有所要求的用戶名的用戶
.filter(u -> u.getUsername().equals(username))
//如果有這樣一個用戶,則將其返回
.findFirst()
//如果一個具有此用戶名的用戶不存在,則拋出一個異常。
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
loadUserByUsername(String username)方法在用戶列表中搜索給定的用戶名并返回想要的UserDetails實例。如果沒有該用戶名的實例,它會拋出一個UsernameNotFoundException。我們現(xiàn)在可以使用這個實現(xiàn)作為我們的UserDetailsService。下一個列表顯示了我們?nèi)绾卧谂渲妙愔邪阉鳛橐粋€Bean添加,并在其中注冊一個用戶。
清單3.14 UserDetailsService在配置類中被注冊為一個Bean。
package com.laurentiuspilca.ssia.config;
import com.laurentiuspilca.ssia.model.User;
import com.laurentiuspilca.ssia.services.InMemoryUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.List;
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails u = new User("john", "12345", "read");
List<UserDetails> users = List.of(u);
return new InMemoryUserDetailsService(users);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
最后,我們創(chuàng)建一個簡單的端點并測試其實現(xiàn)。下面的列表定義了這個端點。
清單3.15 用于測試實現(xiàn)的端點的定義
package com.laurentiuspilca.ssia.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello";
}
}
當使用cURL調(diào)用端點時,我們觀察到,對于密碼為12345的用戶John,我們得到的是HTTP 200 OK。如果我們使用其他東西,應(yīng)用程序會返回401未授權(quán)。
curl -u john:12345 http://localhost:8080/hello
3.3.3 實現(xiàn)UserDetailsManager合同
在這一節(jié)中,我們討論使用和實現(xiàn)UserDetailsManager接口。這個接口擴展了UserDetailsService合約,并為其增加了更多方法。Spring Security需要UserDetailsService合約來進行授權(quán)。但一般來說,在應(yīng)用程序中,也需要對用戶進行管理。大多數(shù)時候,一個應(yīng)用程序應(yīng)該能夠添加新的用戶或刪除現(xiàn)有的用戶。在這種情況下,我們實現(xiàn)了一個由Spring Security定義的更特殊的接口,即UserDetailsManager。它擴展了UserDetailsService,增加了更多我們需要實現(xiàn)的操作。
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
我們在第二章中使用的InMemoryUserDetailsManager對象實際上是一個UserDetailsManager。當時,我們只考慮到它的UserDetailsService特性,但現(xiàn)在你更明白為什么我們能夠在實例上調(diào)用createUser()方法。
3.3.4 使用jdbcuserdetailsmanager進行用戶管理
在InMemoryUserDetailsManager之外,我們經(jīng)常使用另一個UserDetailManager,即JdbcUserDetailsManager。JdbcUserDetailsManager在SQL數(shù)據(jù)庫中管理用戶。它通過JDBC直接連接到數(shù)據(jù)庫。這樣,JdbcUserDetailsManager獨立于任何其他與數(shù)據(jù)庫連接有關(guān)的框架或規(guī)范。
為了理解JdbcUserDetailsManager是如何工作的,最好是通過一個例子將其付諸實施。在下面的例子中,你將實現(xiàn)一個應(yīng)用程序,使用JdbcUserDetailsManager來管理MySQL數(shù)據(jù)庫中的用戶。圖3.4概述了JdbcUserDetailsManager的實現(xiàn)在認證流程中的位置。
你將通過創(chuàng)建一個數(shù)據(jù)庫和兩個表來開始我們關(guān)于如何使用JdbcUserDetailsManager的演示應(yīng)用程序。在我們的例子中,我們將數(shù)據(jù)庫命名為spring,并將其中一個表命名為users,另一個命名為authorities。這些名字是JdbcUserDetailsManager所知道的默認表名。正如你將在本節(jié)末尾學(xué)到的,JdbcUserDetailsManager的實現(xiàn)很靈活,如果你想的話,可以覆蓋這些默認的名字。users表的目的是為了保存用戶記錄。JdbcUserDetails Manager的實現(xiàn)希望在用戶表中有三列:一個用戶名、一個密碼和啟用,你可以用它來停用用戶。
圖3.4 Spring Security的認證流程。這里我們使用一個JDBCUserDetailsManager作為我們的UserDetailsService組件。JdbcUserDetailsManager使用一個數(shù)據(jù)庫來管理用戶。
你可以選擇使用你的數(shù)據(jù)庫管理系統(tǒng)(DBMS)的命令行工具或客戶端應(yīng)用程序來自己創(chuàng)建數(shù)據(jù)庫及其結(jié)構(gòu)。例如,對于MySQL,你可以選擇使用MySQL Workbench來做這件事。但最簡單的是讓Spring Boot自己為你運行腳本。要做到這一點,只需在項目的資源文件夾中再添加兩個文件:schema.sql 和 data.sql。在schema.sql文件中,你可以添加與數(shù)據(jù)庫結(jié)構(gòu)有關(guān)的查詢,如創(chuàng)建、更改或刪除表。在data.sql文件中,你添加與表內(nèi)數(shù)據(jù)有關(guān)的查詢,如INSERT、UPDATE或DELETE。當你啟動應(yīng)用程序時,Spring Boot會自動為你運行這些文件。對于構(gòu)建需要數(shù)據(jù)庫的例子,一個更簡單的解決方案是使用H2內(nèi)存數(shù)據(jù)庫。這樣,你就不需要安裝一個單獨的DBMS解決方案。
注意 如果你愿意,在開發(fā)本書所介紹的應(yīng)用程序時,你也可以用H2。我選擇用一個外部DBMS來實現(xiàn)這些例子,以明確它是系統(tǒng)的一個外部組件,這樣可以避免混淆。
你使用下一個列表中的代碼,用MySQL服務(wù)器創(chuàng)建用戶表。你可以把這個腳本添加到Spring Boot項目的schema.sql文件中。
清單3.16 創(chuàng)建用戶表的SQL查詢
CREATE TABLE IF NOT EXISTS `spring`.`users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`password` VARCHAR(45) NOT NULL,
`enabled` INT NOT NULL,
PRIMARY KEY (`id`));
權(quán)限表存儲每個用戶的權(quán)限。每條記錄存儲一個用戶名和為該用戶名的用戶授予的權(quán)限。
清單3.17 創(chuàng)建權(quán)限表的SQL查詢
CREATE TABLE IF NOT EXISTS `spring`.`authorities` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`authority` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`));
注意 為了簡單起見,在本書提供的例子中,我跳過了對索引或外鍵的定義。
為了確保你有一個用于測試的用戶,在每個表中插入一條記錄。你可以在Spring Boot項目的資源文件夾中的data.sql文件中添加這些查詢:
INSERT IGNORE INTO `spring`.`authorities` VALUES (NULL, 'john', 'write');
INSERT IGNORE INTO `spring`.`users` VALUES (NULL, 'john', '12345', '1');
對于你的項目,你需要至少添加以下列表中的依賴項。檢查你的pom.xml文件,確保你添加了這些依賴項。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
注意 在你的例子中,你可以使用任何SQL數(shù)據(jù)庫技術(shù),只要你把正確的JDBC驅(qū)動添加到依賴關(guān)系中。
你可以在項目的application.properties文件中配置數(shù)據(jù)源,或者作為一個單獨的Bean。如果你選擇使用application.properties文件,你需要在該文件中添加以下幾行。
spring.datasource.url=jdbc:mysql://localhost/spring
spring.datasource.username=<your user>
spring.datasource.password=<your password>
在項目的配置類中,你定義了UserDetailsService和 PasswordEncoder。JdbcUserDetailsManager需要DataSource來連接數(shù)據(jù)庫。連接到數(shù)據(jù)庫。數(shù)據(jù)源可以通過方法的一個參數(shù)自動連接。方法的一個參數(shù)(如下面的列表所示)或通過類的一個屬性來自動連接數(shù)據(jù)源。
清單3.19 在配置類中注冊JdbcUserDetailsManager
package com.laurentiuspilca.ssia.config;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
要訪問應(yīng)用程序的任何端點,你現(xiàn)在需要使用HTTP Basic認證,并使用存儲在數(shù)據(jù)庫中的一個用戶。為了證明這一點,我們創(chuàng)建一個新的端點,如下表所示,然后用cURL調(diào)用它。
package com.laurentiuspilca.ssia.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
在下一個代碼片斷中,你會發(fā)現(xiàn)用正確的用戶名和密碼調(diào)用端點時的結(jié)果。
curl -u john:12345 http://localhost:8080/hello
Hello!
JdbcUserDetailsManager還允許你配置使用的查詢。在前面的例子中,我們確保為表和列使用了準確的名稱,因為JdbcUserDetailsManager的實現(xiàn)期待這些名稱。但是對于你的應(yīng)用程序來說,這些名字可能不是最好的選擇。接下來的列表顯示了如何覆蓋JdbcUserDetailsManager的查詢。
清單3.21 改變JdbcUserDetailsManager的查詢以找到用戶
package com.laurentiuspilca.ssia.config;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
String usersByUsernameQuery = "select username, password, enabled from spring.users where username = ?";
String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
var userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
以同樣的方式,我們可以改變JdbcUserDetailsManager實現(xiàn)所使用的所有查詢。
練習(xí)。編寫一個類似的應(yīng)用程序,在數(shù)據(jù)庫中以不同的方式命名表和列。覆蓋JdbcUserDetailsManager實現(xiàn)的查詢
使用ldapuserdetailsmanager進行用戶管理
Spring Security還為LDAP提供了一個UserDetailsManager的實現(xiàn)。盡管它沒有JdbcUserDetailsManager那么流行,但如果你需要與LDAP系統(tǒng)集成進行用戶管理,你可以信賴它。在項目sia- ch3-ex3中,你可以找到一個使用LdapUserDetailsManager的簡單演示。因為我不能在這個演示中使用真正的LDAP服務(wù)器,我在我的Spring Boot應(yīng)用程序中設(shè)置了一個嵌入式的LDAP服務(wù)器。為了設(shè)置嵌入式LDAP服務(wù)器,我定義了一個簡單的LDAP數(shù)據(jù)交換格式(LDIF)文件。下面的列表顯示了我的LDIF文件的內(nèi)容。
#定義了基礎(chǔ)實體
dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework
#定義了一個group實體
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
#定義一個用戶
dn: uid=john,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John
sn: John
uid: john
userPassword: 12345
在LDIF文件中,我只添加了一個用戶,在這個例子的最后,我們需要測試應(yīng)用程序的行為。我們可以直接將LDIF文件添加到資源文件夾中。這樣,它就自動在classpath中,所以我們以后可以很容易地引用它。我把這個LDIF文件命名為server.ldif。為了與LDAP一起工作,并允許Spring Boot啟動嵌入式LDAP服務(wù)器,你需要將pom.xml添加到依賴項中,如下面的代碼片段。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
在application.properties文件中,你還需要添加嵌入式LDAP服務(wù)器的配置,如下圖代碼片段所示。應(yīng)用程序需要啟動嵌入式LDAP服務(wù)器的值包括LDIF文件的位置、LDAP服務(wù)器的端口和基礎(chǔ)域組件(DN)標簽值。
spring.ldap.embedded.ldif=classpath:server.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
spring.ldap.embedded.port=33389
一旦你有一個用于認證的LDAP服務(wù)器,你就可以配置你的應(yīng)用程序來使用它。下一個列表顯示了如何配置LdapUserDetailsManager,使你的應(yīng)用程序能夠通過LDAP服務(wù)器認證用戶。
清單3.23 配置文件中LdapUserDetailsManager的定義
package com.laurentiuspilca.ssia.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.DefaultLdapUsernameToDnMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.LdapUserDetailsManager;
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
//在Spring上下文中添加一個UserDetailsService實現(xiàn)。
@Override
@Bean
public UserDetailsService userDetailsService() {
//創(chuàng)建一個上下文源,指定LDAP服務(wù)器的地址。
var cs = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:33389/dc=springframework,dc=org");
cs.afterPropertiesSet();
//創(chuàng)建LdapUserDetailsManager實例。
LdapUserDetailsManager manager = new LdapUserDetailsManager(cs);
//設(shè)置一個用戶名映射器,指示LdapUserDetailsManager如何搜索用戶。
manager.setUsernameMapper(
new DefaultLdapUsernameToDnMapper("ou=groups", "uid"));
manager.setGroupSearchBase("ou=groups");
return manager;
}
//設(shè)置應(yīng)用程序需要搜索用戶的群體搜索基礎(chǔ)
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
讓我們也創(chuàng)建一個簡單的端點來測試安全配置。我添加了一個controller類,如下面的代碼片斷中所示。
package com.laurentiuspilca.ssia.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
現(xiàn)在啟動應(yīng)用程序并調(diào)用/hello端點。如果你想讓應(yīng)用程序允許你調(diào)用端點,你需要用用戶John進行認證。接下來的代碼片段向你展示了用cURL調(diào)用端點的結(jié)果。文章來源:http://www.zghlxwxcb.cn/news/detail-731548.html
curl -u john:12345 http://localhost:8080/hello
Hello!
總結(jié)文章來源地址http://www.zghlxwxcb.cn/news/detail-731548.html
- UserDetails接口是你用來描述Spring Security中用戶的契約。
- UserDetailsService接口是Spring Security期望你在認證架構(gòu)中實現(xiàn)的契約,以描述應(yīng)用程序獲取用戶詳細信息的方式。
- UserDetailsManager接口擴展了UserDetailsService,并增加了與創(chuàng)建、改變或刪除用戶有關(guān)的行為。
- Spring Security提供了UserDetailsManager合約的一些實現(xiàn)。其中包括InMemoryUserDetailsManager、JdbcUserDetailsManager和LdapUserDetailsManager。
- JdbcUserDetailsManager的優(yōu)點是直接使用JDBC,不會將應(yīng)用程序鎖定在其他框架中。
到了這里,關(guān)于Spring Security in Action 第三章 SpringSecurity管理用戶的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!