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