天咱們來聊一聊 Spring Security 中的表決機(jī)制與投票器。
當(dāng)用戶想訪問 Spring Security 中一個(gè)受保護(hù)的資源時(shí),用戶具備一些角色,該資源的訪問也需要一些角色,在比對(duì)用戶具備的角色和資源需要的角色時(shí),就會(huì)用到投票器和表決機(jī)制。
當(dāng)用戶想要訪問某一個(gè)資源時(shí),投票器根據(jù)用戶的角色投出贊成或者反對(duì)票,表決方式則根據(jù)投票器的結(jié)果進(jìn)行表決。
在 Spring Security 中,默認(rèn)提供了三種表決機(jī)制,當(dāng)然,我們也可以不用系統(tǒng)提供的表決機(jī)制和投票器,而是完全自己來定義,這也是可以的。
本文松哥將和大家重點(diǎn)介紹三種表決機(jī)制和默認(rèn)的投票器。
先來看投票器。
在 Spring Security 中,投票器是由 AccessDecisionVoter 接口來規(guī)范的,我們來看下 AccessDecisionVoter 接口的實(shí)現(xiàn):
可以看到,投票器的實(shí)現(xiàn)有好多種,我們可以選擇其中一種或多種投票器,也可以自定義投票器,默認(rèn)的投票器是 WebExpressionVoter。
我們來看 AccessDecisionVoter 的定義:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
我稍微解釋下:
我們來分別看下幾個(gè)投票器的實(shí)現(xiàn)。
RoleVoter 主要用來判斷當(dāng)前請(qǐng)求是否具備該接口所需要的角色,我們來看下其 vote 方法:
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
這個(gè)方法的判斷邏輯很簡(jiǎn)單,如果當(dāng)前登錄主體為 null,則直接返回 ACCESS_DENIED 表示拒絕訪問;否則就從當(dāng)前登錄主體 authentication 中抽取出角色信息,然后和 attributes 進(jìn)行對(duì)比,如果具備 attributes 中所需角色的任意一種,則返回 ACCESS_GRANTED 表示允許訪問。例如 attributes 中的角色為 [a,b,c],當(dāng)前用戶具備 a,則允許訪問,不需要三種角色同時(shí)具備。
另外還有一個(gè)需要注意的地方,就是 RoleVoter 的 supports 方法,我們來看下:
public class RoleVoter implements AccessDecisionVoter<Object> {
private String rolePrefix = "ROLE_";
public String getRolePrefix() {
return rolePrefix;
}
public void setRolePrefix(String rolePrefix) {
this.rolePrefix = rolePrefix;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& attribute.getAttribute().startsWith(getRolePrefix())) {
return true;
}
else {
return false;
}
}
public boolean supports(Class<?> clazz) {
return true;
}
}
可以看到,這里涉及到了一個(gè) rolePrefix 前綴,這個(gè)前綴是 ROLE_,在 supports 方法中,只有主體角色前綴是 ROLE_,這個(gè) supoorts 方法才會(huì)返回 true,這個(gè)投票器才會(huì)生效。
RoleHierarchyVoter 是 RoleVoter 的一個(gè)子類,在 RoleVoter 角色判斷的基礎(chǔ)上,引入了角色分層管理,也就是角色繼承,關(guān)于角色繼承,小伙伴們可以參考松哥之前的文章(Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限?)。
RoleHierarchyVoter 類的 vote 方法和 RoleVoter 一致,唯一的區(qū)別在于 RoleHierarchyVoter 類重寫了 extractAuthorities 方法。
@Override
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return roleHierarchy.getReachableGrantedAuthorities(authentication
.getAuthorities());
}
角色分層之后,需要通過 getReachableGrantedAuthorities 方法獲取實(shí)際具備的角色,具體請(qǐng)參考:Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限? 一文。
這是一個(gè)基于表達(dá)式權(quán)限控制的投票器,松哥后面專門花點(diǎn)時(shí)間和小伙伴們聊一聊基于表達(dá)式的權(quán)限控制,這里我們先不做過多展開,簡(jiǎn)單看下它的 vote 方法:
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
如果你熟練使用 SpEL 的話,這段代碼應(yīng)該說還是很好理解的,不過根據(jù)我的經(jīng)驗(yàn),實(shí)際工作中用到 SpEL 場(chǎng)景雖然有,但是不多,所以可能有很多小伙伴并不了解 SpEL 的用法,這個(gè)需要小伙伴們自行復(fù)習(xí)下,我也給大家推薦一篇還不錯(cuò)的文章:https://www.cnblogs.com/larryzeal/p/5964621.html。
這里代碼實(shí)際上就是根據(jù)傳入的 attributes 屬性構(gòu)建 weca 對(duì)象,然后根據(jù)傳入的 authentication 參數(shù)構(gòu)建 ctx 對(duì)象,最后調(diào)用 evaluateAsBoolean 方法去判斷權(quán)限是否匹配。
上面介紹這三個(gè)投票器是我們?cè)趯?shí)際開發(fā)中使用較多的三個(gè)。
另外還有幾個(gè)比較冷門的投票器,松哥也稍微說下,小伙伴們了解下。
Jsr250Voter
處理 Jsr-250 權(quán)限注解的投票器,如 @PermitAll,@DenyAll 等。
AuthenticatedVoter
AuthenticatedVoter 用于判斷 ConfigAttribute 上是否擁有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三種角色。
IS_AUTHENTICATED_FULLY 表示當(dāng)前認(rèn)證用戶必須是通過用戶名/密碼的方式認(rèn)證的,通過 RememberMe 的方式認(rèn)證無效。
IS_AUTHENTICATED_REMEMBERED 表示當(dāng)前登錄用戶必須是通過 RememberMe 的方式完成認(rèn)證的。
IS_AUTHENTICATED_ANONYMOUSLY 表示當(dāng)前登錄用戶必須是匿名用戶。
當(dāng)項(xiàng)目引入 RememberMe 并且想?yún)^(qū)分不同的認(rèn)證方式時(shí),可以考慮這個(gè)投票器。
AbstractAclVoter
提供編寫域?qū)ο?ACL 選項(xiàng)的幫助方法,沒有綁定到任何特定的 ACL 系統(tǒng)。
PreInvocationAuthorizationAdviceVoter
使用 @PreFilter 和 @PreAuthorize 注解處理的權(quán)限,通過 PreInvocationAuthorizationAdvice 來授權(quán)。
當(dāng)然,如果這些投票器不能滿足需求,也可以自定義。
一個(gè)請(qǐng)求不一定只有一個(gè)投票器,也可能有多個(gè)投票器,所以在投票器的基礎(chǔ)上我們還需要表決機(jī)制。
表決相關(guān)的類主要是三個(gè):
他們的繼承關(guān)系如上圖。
三個(gè)決策器都會(huì)把項(xiàng)目中的所有投票器調(diào)用一遍,默認(rèn)使用的決策器是 AffirmativeBased。
三個(gè)決策器的區(qū)別如下:
這里的具體判斷邏輯比較簡(jiǎn)單,松哥就不貼源碼了,感興趣的小伙伴可以自己看看。
當(dāng)我們使用基于表達(dá)式的權(quán)限控制時(shí),像下面這樣:
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().fullyAuthenticated()
那么默認(rèn)的投票器和決策器是在 AbstractInterceptUrlConfigurer#createDefaultAccessDecisionManager 方法中配置的:
private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
return postProcess(result);
}
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(getExpressionHandler(http));
decisionVoters.add(expressionVoter);
return decisionVoters;
}
這里就可以看到默認(rèn)的決策器和投票器,并且決策器 AffirmativeBased 對(duì)象創(chuàng)建好之后,還調(diào)用 postProcess 方法注冊(cè)到 Spring 容器中去了,結(jié)合松哥本系列前面的文章,大家知道,如果我們想要修改該對(duì)象就非常容易了:
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().fullyAuthenticated()
.withObjectPostProcessor(new ObjectPostProcessor<AffirmativeBased>() {
@Override
public <O extends AffirmativeBased> O postProcess(O object) {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleHierarchyVoter(roleHierarchy()));
AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters);
return (O) affirmativeBased;
}
})
.and()
.csrf()
.disable();
這里只是給大家一個(gè)演示,正常來說我們是不需要這樣修改的。當(dāng)我們使用不同的權(quán)限配置方式時(shí),會(huì)有自動(dòng)配置對(duì)應(yīng)的投票器和決策器。或者我們手動(dòng)配置投票器和決策器,如果是系統(tǒng)配置好的,大部分情況下并不需要我們修改。
本文主要和小伙伴們簡(jiǎn)單分享一下 Spring Security 中的投票器和決策器,關(guān)于授權(quán)的更多知識(shí),松哥下篇文章繼續(xù)和小伙伴們細(xì)聊。
天是被《即刻電音》相關(guān)熱搜洗版的一天。
上到節(jié)目主理人,下到主理人粉絲,甚至連主理人的經(jīng)紀(jì)人都沒落下。
可能是節(jié)目開播以來喜提熱搜最多的一次。
到底怎么回事呢?
原來是《即刻電音》的錄制現(xiàn)場(chǎng)發(fā)生了極大的騷亂。
來看一個(gè)張藝興粉絲發(fā)來的repo▽
簡(jiǎn)單總結(jié)一下:
1.大張偉粉絲覺得節(jié)目投票有問題,一直在喊“黑幕”
2.大張偉為保隊(duì)員又一次打起了感情牌
3.張藝興想說話卻發(fā)現(xiàn)話筒沒聲,憤怒地喊“開麥”和“宇宙(張藝興隊(duì)名)就是diao”
4.張藝興經(jīng)紀(jì)人上場(chǎng)搶走張藝興話筒,并說大張偉粉絲輸不起就別玩
插播一條,給沒看過《即刻電音》的朋友們補(bǔ)補(bǔ)課。
張藝興粉絲控訴的打感情牌其實(shí)是指大張偉在節(jié)目中哭訴的事。
當(dāng)時(shí)因?yàn)橛羞x手對(duì)淘汰的規(guī)則表示不滿,大張偉搬出自己的例子來進(jìn)行說服。
過程中他一度哽咽地說:“我的心告訴我應(yīng)該退出這個(gè)行業(yè),但我一直都在這個(gè)行業(yè)的原因就是因?yàn)槲覑垡魳贰?strong>我有很多問題,但是也有太多人仗著我喜歡音樂欺負(fù)我。”
用笑遮掩自己的情緒失控就還挺令人動(dòng)容的▽
OK,補(bǔ)完課再回到兩家粉絲撕X的事。
咱們也不能只偏聽一方觀點(diǎn),再來看一下大張偉粉絲方面關(guān)于錄制現(xiàn)場(chǎng)的repo▽
過程上的描述和張藝興粉絲那邊的說法基本相同。
但細(xì)節(jié)上進(jìn)行了一些反駁,比如:
1.大張偉粉絲質(zhì)疑節(jié)目組黑幕是因?yàn)閺牡诙嗛_始就發(fā)現(xiàn)投票器有問題
2.大張偉情緒激動(dòng)是因?yàn)樗膱F(tuán)隊(duì)面臨團(tuán)滅的風(fēng)險(xiǎn),同時(shí)質(zhì)問導(dǎo)演組:“錢我可以不要了,但你們要有底線。”
3.大張偉粉絲罵節(jié)目組的話被路過的張藝興聽到了,并回頭問大張偉粉絲“誰罵我”
4.張藝興確實(shí)大喊開麥被楊天真攔下了,但當(dāng)時(shí)本來就要退場(chǎng)了,所有人的麥都會(huì)被收
也是個(gè)公說公有理,婆說婆有理的“羅生門”。
不過,讓張藝興粉絲生氣的其中一個(gè)原因還在于大張偉粉絲在節(jié)目現(xiàn)場(chǎng)上升愛豆。
據(jù)張藝興粉絲描述,當(dāng)時(shí)看張藝興情緒激動(dòng),大張偉粉絲不僅沒有收斂,還冷嘲熱諷罵張藝興犯賤。
說:“見過撿錢的,見過撿東西的,沒見過撿罵的。”
但大張偉粉絲則說,他們壓根沒罵張藝興,罵的是這個(gè)XX的世界,結(jié)果恰好被路過的張藝興聽到。
由此還衍生出了一個(gè)關(guān)于張藝興從大張偉粉絲全世界路過的段子...
更抓馬的部分在于,兩方粉絲都說是第一次見自家愛豆這么生氣。
粉絲的賣慘邏輯就真的都很相似。
大張偉粉絲還痛心疾首稱:“我第一次見大張偉當(dāng)眾給自己一個(gè)耳光,這要怎么才能下得去手?”
本來張藝興粉絲就一直在斥責(zé)大張偉賣慘了,這么一說不是在給對(duì)家送錘送人頭嗎...
于是,其他粉絲只好一而再再而三地進(jìn)行澄清。
替他們感到心累。
然而,就在雙方爭(zhēng)得不可開交之時(shí),張藝興粉絲卻開始一條接一條地甩出錄制現(xiàn)場(chǎng)的各種音頻。
要知道,節(jié)目錄制是不能帶這些電子設(shè)備進(jìn)去的。
真不愧是經(jīng)驗(yàn)老道的流量粉。
M@https://v.youku.com/v_show/id_XMzk4OTUxNTQzNg==.html@M
根據(jù)音頻來看,大張偉確實(shí)哽咽了▽
大張偉發(fā)言完畢后,他的粉絲用很大的聲音喊“黑幕”▽
這時(shí),輪到張藝興隊(duì)的隊(duì)員tsunano進(jìn)行發(fā)言。
他說想給大家講一個(gè)小故事。
但臺(tái)下卻一直情緒激動(dòng)地在喊“不聽”▽
tsunano試圖將故事繼續(xù)講下去,背景音卻全是讓他閉嘴的聲音▽
這些“不聽”、“閉嘴”的抗議聲甚至大到大張偉不得不出面調(diào)節(jié)的程度▽
而在結(jié)果公布后,節(jié)目組應(yīng)該是有讓主理人進(jìn)到候場(chǎng)區(qū)之類的地方進(jìn)行休息或是調(diào)試。
只聽到錄制重新開始,導(dǎo)演讓主理人回到各自的位置。
就在這時(shí),張藝興突然大喊了幾聲開麥。
開麥后,就有了他感慨“wow”。
緊接著就是repo種所說的張藝興歇斯底里的那句“宇宙隊(duì)就是最diao的”。
之前也有人說是大張偉煽動(dòng)了現(xiàn)場(chǎng)的粉絲情緒。
在被曝光的另一段錄音當(dāng)中,有粉絲錄到休息期間,大張偉的確帶著情緒對(duì)張藝興粉絲 說了“你們牛逼”。
但橘子君聽完音頻覺得倒也不像一些repo當(dāng)中說的那樣是朝張藝興粉絲喊話。
當(dāng)時(shí)大張偉并沒有帶麥,算是正常講話的聲音大小,應(yīng)該只有附近一部分粉絲能聽見。
萬萬沒想到,錄個(gè)節(jié)目竟然能錄到兩位在娛樂圈摸爬滾打多年的藝人全都情緒失控...
只能說,大家就還挺真情實(shí)感的。
聽一聽音頻就知道了,現(xiàn)場(chǎng)氛圍真的就跟打群架似的,來一把火隨時(shí)能著...
面對(duì)如此窘境,主持人只好先cue掌握著最主要投票權(quán)的專業(yè)音樂評(píng)審回應(yīng)黑幕問題▽
看觀眾情緒依然未得到任和平復(fù),主持人又出面了。
他說:“這個(gè)節(jié)目沒有黑幕,也沒必要有黑幕。你覺得你投票器壞了那一票就能夠讓整個(gè)節(jié)目的結(jié)果有所改變嗎?你覺得所有你們投票器壞了就針對(duì)你們了嗎?”
嗯嗯嗯???
主持人不應(yīng)該是起到安撫作用嗎,怎么所說的話還這么具有煽動(dòng)性呢???
很多比賽就是會(huì)因?yàn)橐黄敝顩Q定命運(yùn)啊,難道不是每一票都很重要嗎???
不過,從專業(yè)評(píng)審和主持人的兩段話來看,《即刻電音》這個(gè)節(jié)目似乎還真的存在一些問題。
投票器壞掉也就算了,專業(yè)評(píng)審竟然說自己沒有完整看到選手的演出???
到底咋回事啊...
不是說好的公平公正嗎[捂臉]...
所以,也怪不得兩邊粉絲會(huì)這樣爆發(fā)。
連節(jié)目中的另一位主理人尚雯婕都在節(jié)目錄完的第一時(shí)間無語地在微博發(fā)了個(gè)“呵呵”。
再來說說《即刻電音》這個(gè)節(jié)目。
別看它電音的題材小眾,但每期的話題還真不比爆款節(jié)目來得少。
第一期就因?yàn)檫x手diss大張偉而引發(fā)討論。
當(dāng)時(shí),選手Anti-General說自己一聽大張偉要來參加節(jié)目,他的第一反應(yīng)就是退賽。
節(jié)目一播出,Anti-General就因?yàn)檫@番言論被罵很慘。
隨后,他又在微博發(fā)了長(zhǎng)文進(jìn)行解釋。
為什么會(huì)在節(jié)目里這樣說?
一是因?yàn)槌u事件,二是因?yàn)榇髲垈ピ诠?jié)目中所表演的曲目并不能讓他認(rèn)同。
雖然大張偉在節(jié)目中的一些專業(yè)表現(xiàn)讓他的印象有所改觀,但他還是認(rèn)為大張偉在用自己做不到的事要求別人。
他理解大張偉所承受的精神壓力,但又認(rèn)為這些壓力都是因?yàn)榇髲垈ミ€沒能做出服眾的作品,那大家就只能把以前的事翻出來講。
最后,Anti-General還很嗆地放話:至于大老師你這次能不能洗白,我們拭目以待。
當(dāng)時(shí)大張偉也有回應(yīng),他說:
“一個(gè)常年在做音樂的人,即便不善言表,答起這個(gè)‘自己’的問題也會(huì)滔滔不絕。”
“我音樂中的自己就是DM48的游樂園,就是陽光彩虹小白馬,就是必須熱血有趣又可愛。”
算是解答了Anti-General說他“用自己做不到的事要求別人”的這部分▽
但選手所提出的洗白質(zhì)疑依然依然是目前《即刻電音》為人詬病的問題之一。
大概是因?yàn)槌31徽f抄襲,所以大張偉在《即刻電音》中一直都沖在抓抄襲的第一線。
選手表演完,他立刻就說跟某樂隊(duì)的音樂有些相似▽
選手承認(rèn)有借鑒,但并不承認(rèn)抄襲▽
特邀主理人也認(rèn)同大張偉說的觀點(diǎn)▽
大張偉則表示,自己之所以會(huì)說這段話,是因?yàn)橛袝r(shí)候大家所認(rèn)為的瑕疵,“天都會(huì)原諒你,但是網(wǎng)友不會(huì)”。
再加上開頭我們放過的那段大張偉哭訴截圖,理所當(dāng)然有不少人會(huì)質(zhì)疑這節(jié)目是在給大張偉洗白。
而針對(duì)洗白質(zhì)疑,大張偉自己則說:“我根本沒必要洗白,因?yàn)槲以缇褪遣噬牧恕?/strong>”
除此之外,圍繞張藝興的主要爭(zhēng)議則是上次讓馮提莫晉級(jí)。
馮提莫大家應(yīng)該都并不陌生,印象中她跟電音沒扯上過啥關(guān)系。
但她卻來參加了這檔節(jié)目...
張藝興看完表演后很為難,他先是做了這樣一個(gè)雙手抱頭的動(dòng)作,說了一句“嘶...喔...這個(gè)......這個(gè)怎么說呢”▽
然后給出了這樣的評(píng)價(jià):
“你們選的是未來感,沒有讓我覺得很未來啊。”
這時(shí)候和馮提莫一起合作的KK張站出來幫馮提莫講話,說“想要幫她制作和改編歌曲,所以在編曲上用了大量未來的聲音設(shè)計(jì)......”
藝興終于聽不下去了,舉手打斷:
“等一下,對(duì)不起打斷你一下,就是我覺得就很普通,一般的電子舞曲的歌曲就是這樣的,我沒有覺得有什么很未來的東西。”
“你是一個(gè)很好的歌手,你也是一個(gè)好的制作人,就是呈現(xiàn)出來的東西有點(diǎn)差強(qiáng)人意。”
到現(xiàn)在為止,張藝興的態(tài)度已經(jīng)很明顯了吧,他并不滿意這個(gè)組合的表演。
按照這個(gè)走向,他接下來會(huì)很自然地“淘汰”他們才對(duì)。
然而最終的結(jié)果是,他給了“推薦”(滿臉寫著開心)▽
粉絲們也看不過去了,在評(píng)論里放出了錄制當(dāng)天的“實(shí)情”。
說是在導(dǎo)演勸說下張藝興才會(huì)讓馮提莫晉級(jí)。
但張藝興自己則說“是我做的決定,導(dǎo)演組沒有逼我”。
但也并沒有多少人相信就是了。
熱評(píng)都是“被綁架了你就眨眨眼”...
-------------------------
到目前為止《即刻電音》一共才播了5期,爭(zhēng)議話題就已經(jīng)有前面說的這么多了。
爭(zhēng)議程度怕是跟當(dāng)年的《花少2》都有得一拼了吧?
還不知道最新錄制的這一期已經(jīng)亂到這種程度后期要怎么剪。
吃瓜...
最后一句
能做的不多,給后期買點(diǎn)防脫洗發(fā)水吧。
Spring Security 是一種基于 Spring AOP 和 Servlet 過濾器的安全框架。原名Acegi Security,最早于2003年起源于Spring社區(qū)。 2007年末正式歸為Spring Framework的正式子項(xiàng)目,并改名為Spring Security 。此后, Spring Security有了長(zhǎng)遠(yuǎn)發(fā)展,現(xiàn)已成為一款基于Spring Framework的廣泛應(yīng)用的安全框架,主要為應(yīng)用服務(wù)提供用戶認(rèn)證(Authentication)和用戶授權(quán)(Authorization)功能。詳情可參考Spring Security 官方文檔。
Spring Security 針對(duì)安全方面的兩大難題, 鑒權(quán)(Authentication)和授權(quán)(Authorization)提供了靈活強(qiáng)大的解決方案。
Spring Security優(yōu)勢(shì):
如果項(xiàng)目需要安全控制功能,不用自己去實(shí)現(xiàn)一套, 集成Spring Security專業(yè)安全框架是首選, 適用后臺(tái)管理、接口資源管理、微服務(wù)統(tǒng)一鑒權(quán)等場(chǎng)景。
處理流程圖:
類名 | 描述 |
AffirmativeBased | 只要有一個(gè)投票器允許訪問, 請(qǐng)求立刻允許放行, 不管之前存在拒絕的決定 |
ConsensusBased | 多數(shù)票機(jī)制(允許或拒絕),多數(shù)的一方?jīng)Q定了AccessDecisionManager的結(jié)果。平局的投票和空票(全部棄權(quán))的結(jié)果是可配置的 |
UnanimousBased | 所有的投票器必須全部是允許的, 否則訪問將被拒絕 |
Spring Boot 與Spring Security 集成, 包含一般集成用法, 還包括自定義用戶登陸頁面使用, 自定義內(nèi)存模式驗(yàn)證, 以及自定義登陸成功與失敗邏輯處理。
1、創(chuàng)建工程spring-boot-security-integrate
啟動(dòng)類:
com.mirson.spring.boot.security.integrate.startup.SecurityIntegrateApplication
@SpringBootApplication
@ComponentScan(basePackages = {"com.mirson"})
public class SecurityIntegrateApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityIntegrateApplication.class, args);
}
}
2、MAVEN依賴
<dependencies>
<!-- Spring Boot Security 安全依賴組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Thymeleaf 模板依賴組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Boot Web 依賴組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring boot freemarker 自動(dòng)化配置組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
3、定義一個(gè)外部訪問接口
1)創(chuàng)建實(shí)體
定義一個(gè)用戶實(shí)體
com.mirson.spring.boot.security.integrate.po.User
@Data
public class User {
/**
* ID
*/
private Integer id;
/**
* 用戶名稱
*/
private String name;
/**
* 年齡
*/
private String age;
/**
* 省份
*/
private String province;
/**
* 創(chuàng)建時(shí)間
*/
private Date createDate;
}
2)提供Web訪問接口
提供一個(gè)獲取用戶信息接口
com.mirson.spring.boot.security.integrate.controller.UserController
@RestController
@RequestMapping("/user")
@Log4j2
public class UserController {
@GetMapping("/getUserInfo")
@ResponseBody
public User getUserInfo() {
User user = new User();
user.setId(0);
user.setAge("21");
user.setName("user1");
user.setCreateDate(new Date());
return user;
}
}
4、工程配置
server:
port: 22618
spring:
application:
name: security-integrate
# security 安全配置
security:
user:
name: "admin"
password: "admin"
設(shè)置默認(rèn)的用戶名與密碼為admin。
5、功能驗(yàn)證
1) 請(qǐng)求獲取用戶信息接口
訪問接口: http://127.0.0.1:22618/getUserInfo
沒有鑒權(quán)的情況下, 會(huì)出現(xiàn)登陸界面。
2)輸入用戶信息admin/admin, 再次請(qǐng)求獲取用戶信息接口
正確輸入用戶信息后, 可以正常訪問用戶信息接口。
Spring Security 內(nèi)置會(huì)有一套登陸頁面, 也可以自定修改, 這里通過freemark模板來實(shí)現(xiàn)自定登陸頁面渲染。
application.yml增加配置:
# freemarker 模板配置
freemarker:
allow-request-override: false
allow-session-override: false
cache: true
charset: UTF-8
check-template-location: true
content-type: text/html
enabled: true
expose-request-attributes: false
expose-session-attributes: false
expose-spring-macro-helpers: true
prefer-file-system-access: true
suffix: .ftl
template-loader-path: classpath:/templates/
增加freemark模板文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>自定義系統(tǒng)登陸</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/signin.css" rel="stylesheet">
</head>
<body>
<div class="container form-margin-top">
<form class="form-signin" action="/user/doUserLogin" method="post">
<h2 class="form-signin-heading" align="center">自定義系統(tǒng)登陸</h2>
<input type="text" name="username" class="form-control form-margin-top" placeholder="賬號(hào)" required autofocus>
<input type="password" name="password" class="form-control" placeholder="密碼" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button>
</form>
</div>
<footer>
<p>support by: mirson</p>
</footer>
</body>
</html>
添加CSS靜態(tài)資源文件
添加JAVA CONFIG配置:
com.mirson.spring.boot.security.integrate.config.SpringSecurityConfiguration
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/userLoginForm",
"/css/**").permitAll()
.antMatchers("/user/getUserInfo").authenticated()
.and()
.formLogin()
.loginPage("/user/userLoginForm") //自定義登錄頁面
.permitAll() //允許所有人訪問該路由
.and()
.csrf()
.disable() //暫時(shí)禁用csrc否則無法提交
.httpBasic();
}
}
重啟, 訪問接口: http://127.0.0.1:22618/user/getUserInfo
會(huì)自動(dòng)跳轉(zhuǎn)到自定義的登陸頁面:
修改com.mirson.spring.boot.security.integrate.config.SpringSecurityConfiguration配置:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/doUserLogin", "/user/userLoginForm",
"/css/**").permitAll()
.antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/user/userLoginForm") //自定義登錄頁面
.loginProcessingUrl("/user/doUserLogin") // 自定義登陸處理地址
.permitAll() //允許所有人訪問該路由
.and()
.csrf()
.disable() //暫時(shí)禁用csrc否則無法提交
.httpBasic();
}
}
通過Spring Security 可以控制, 哪些資源需要受權(quán)限保護(hù), 哪些可以開放訪問。
/user/userLoginForm 用戶登陸頁面
/user/doUserLogin 登陸處理地址
/css/** 靜態(tài)資源文件
/user/getUserInfo 獲取用戶信息接口
可以指定Role角色權(quán)限, 不指定, 只要登陸即擁有訪問權(quán)限。
loginPage指定/user/userLoginForm為自定義登陸頁面;
loginProcessingUrl為登陸請(qǐng)求處理接口地址,可以不用對(duì)其做具體實(shí)現(xiàn),Spring Security 會(huì)做默認(rèn)處理。
1、創(chuàng)建鑒權(quán)用戶對(duì)象:
com.mirson.spring.boot.security.integrate.po.OAuthUser
@Data
public class OAuthUser extends User {
public OAuthUser(String account, String password){
super(account, password, true, true, true, true, Collections.EMPTY_SET);
}
}
繼承的是Spring Security 的User對(duì)象, 將賬號(hào)和密碼信息, 傳遞至父類構(gòu)造方法。
2、修改SpringSecurityConfiguration配置
增加:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 用戶認(rèn)證服務(wù)
* */
@Bean
@Override
protected UserDetailsService userDetailsService() {
//創(chuàng)建基于內(nèi)存用戶管理對(duì)象
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//自定義權(quán)限
Collection<GrantedAuthority> adminAuth = new ArrayList<>();
adminAuth.add(new SimpleGrantedAuthority("ADMIN"));
//自定義用戶
OAuthUser oAuthUser = new OAuthUser("admin", "admin123");
manager.createUser(oAuthUser);
return manager;
}
/**
* 配置密碼編碼器, 不需加密
* @return
*/
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
...
}
采用內(nèi)存模式管理用戶對(duì)象InMemoryUserDetailsManager, 自定義認(rèn)證用戶名和密碼, 分別為admin,admin123。 需要配置密碼編碼器, 可以支持自定義密碼加密方式, 這里不需加密, 配置NoOpPasswordEncoder。
Spring Security 提供了接口, 登陸成功, 可以通過處理器實(shí)現(xiàn)自定義邏輯。 新建com.mirson.spring.boot.security.integrate.handler.SecuritySuccessHandler
@Component
@Log4j2
public class SecuritySuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* 認(rèn)證成功處理
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("Process in SecuritySuccessHandler ==> login success.");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
這里通過自定義登陸成功處理器, 將登陸成功的信息返回客戶端。
將處理器加入到自定義配置SpringSecurityConfiguration中:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/doUserLogin", "/user/userLoginForm",
"/css/**").permitAll()
.antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/user/userLoginForm") //自定義登錄頁面
.loginProcessingUrl("/user/doUserLogin") // 自定義登陸處理地址
.successHandler(securitySuccessHandler) // 自定義登陸成功處理器
.permitAll() //允許所有人訪問該路由
.and()
.csrf()
.disable() //暫時(shí)禁用csrc否則無法提交
.httpBasic();
}
如果登陸失敗, 也可以通過處理器實(shí)現(xiàn)自定義邏輯。 新建com.mirson.spring.boot.security.integrate.handler.SecurityFailureHandler
@Component
@Log4j2
public class SecurityFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
log.info("Process in SecurityFailureHandler ==> login failure.");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
}
}
將登陸失敗的錯(cuò)誤信息, 返回給客戶端。
修改自定義配置SpringSecurityConfiguration:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/doUserLogin", "/user/userLoginForm",
"/css/**").permitAll()
.antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/user/userLoginForm") //自定義登錄頁面
.loginProcessingUrl("/user/doUserLogin") // 自定義登陸處理地址
.successHandler(securitySuccessHandler) // 自定義登陸成功處理器
.failureHandler(securityFailureHandler) // 自定義登陸失敗處理器
.permitAll() //允許所有人訪問該路由
.and()
.csrf()
.disable() //暫時(shí)禁用csrc否則無法提交
.httpBasic();
}
1、驗(yàn)證內(nèi)存模式鑒權(quán)
內(nèi)存模式我們?cè)O(shè)置的用戶名和密碼為admin/admin123
默認(rèn)配置文件是admin/admin
輸入admin/admin123, 成功登陸, 內(nèi)存模式已生效。
2、登陸成功處理器驗(yàn)證
重啟服務(wù), 訪問獲取用戶信息接口, http://127.0.0.1:22618/user/getUserInfo
默認(rèn)是會(huì)跳轉(zhuǎn)到登陸頁面, 如果沒配置登陸成功處理器, 登陸成功后, 會(huì)進(jìn)入上一次訪問頁面
可以看到, 登陸成功后, 并沒有跳轉(zhuǎn)到上一次訪問的用戶信息接口, 而是返回了登陸成功處理器的結(jié)果。
3、登陸失敗處理器驗(yàn)證
同樣, 問獲取用戶信息接口, http://127.0.0.1:22618/user/getUserInfo, 會(huì)自動(dòng)跳轉(zhuǎn)到登陸頁面。
采用錯(cuò)誤的用戶密碼, 返回了登陸失敗處理器的結(jié)果。
Spring Security 提供了鑒權(quán)與授權(quán)的功能支持, 這里做了詳細(xì)講解, 如何使用與配置, 并講解了自定義鑒權(quán)處理功能, 實(shí)際業(yè)務(wù)當(dāng)中,并非一層不變, 會(huì)做不同配置修改,比如自定義資源訪問配置, 不同項(xiàng)目有不同的要求, 掌握這些自定義配置, 基本可以覆蓋主要的業(yè)務(wù)場(chǎng)景, 針對(duì)更復(fù)雜的鑒權(quán), 可以采用oauth2做鑒權(quán)處理, 在后續(xù)教程中會(huì)做講解。
教程源碼下載地址: https://download.csdn.net/download/hxx688/86400104
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。