Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
一、Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證
1、springsecurity是通過在web一系列原生filter攔截器中增加自己的過濾器鏈來攔截web請(qǐng)求,然后請(qǐng)求會(huì)在經(jīng)過過濾器鏈的過程中會(huì)完成認(rèn)證與授權(quán),如果中間發(fā)現(xiàn)這條請(qǐng)求未認(rèn)證或者未授權(quán),會(huì)根據(jù)被保護(hù)API的權(quán)限去拋出異常,然后由異常處理器去處理這些異常。

2、SpringSecurity通過FilterChainProxy管理眾多SecurityFilterChain, 而FilterChainProxy則被DelegatingFilterProxy管理并被DelegatingFilterProxy放入web原生的過濾器鏈中;
每個(gè)SecurityFilterChain下則是具體的擁有攔截規(guī)則的filter,這些filter由SpringSecurity進(jìn)行代理操作,可以理解為他是"Security Filter",而不是原生的"Web Filter";
總結(jié)就是:
【DelegatingFilterProxy】——管理——>【FilterChainProxy】——管理——>【SecurityFilterChain】——管理——>【Security Filter】
3、springboot整合springsecurity,springboot會(huì)通過一系列xxxAutoConfiguration進(jìn)行自動(dòng)配置默認(rèn)的Spring Security的一系列底層組件,如WebSecurityConfigurerAdapter和一些默認(rèn)組件,有些"Security Filter"會(huì)自動(dòng)開啟,有些則不會(huì);
整個(gè)認(rèn)證的過程其實(shí)一直在圍繞圖中過濾鏈的綠色部分,而動(dòng)態(tài)鑒權(quán)主要是圍繞其橙色部分;
Spring Security配置中有兩個(gè)叫formLogin和httpBasic的配置項(xiàng),這兩個(gè)配置項(xiàng)就分別對(duì)應(yīng)著圖中分的兩個(gè)過濾器
- formLogin對(duì)應(yīng)著你form表單認(rèn)證方式,即UsernamePasswordAuthenticationFilter。
- httpBasic對(duì)應(yīng)著Basic認(rèn)證方式,即BasicAuthenticationFilter。

4、我使用的就是UsernamePasswordAuthenticationFilter這個(gè)過濾器,springboot整合springsecurity時(shí)會(huì)自動(dòng)加載這個(gè)過濾器;

Spring Security 在自動(dòng)裝配后,會(huì)有默認(rèn)的攔截策略,未登陸的請(qǐng)求都會(huì)被攔截并跳轉(zhuǎn)到login登錄頁,此時(shí)輸入賬號(hào)密碼登錄就會(huì)被這個(gè)UsernamePasswordAuthenticationFilter攔截,并驗(yàn)證賬號(hào)是否存在,密碼是否正確
進(jìn)入formlogin,發(fā)現(xiàn)有個(gè)**FormLoginConfigurer()**方法

進(jìn)入FormLoginConfigurer()方法,在這里用戶輸入賬號(hào)密碼就會(huì)被這個(gè)UsernamePasswordAuthenticationFilter攔截,并驗(yàn)證進(jìn)行認(rèn)證

發(fā)送登陸請(qǐng)求后,UsernamePasswordAuthenticationFilter會(huì)調(diào)用attemptAuthentication() 方法進(jìn)行認(rèn)證,失敗則拋出異常,成功則返回帶有用戶信息的Authentication對(duì)象

"Security Filter"中,認(rèn)證過程是由 " 主角 " AuthenticationManager(接口)去管理AuthenticationProvider(接口)去實(shí)現(xiàn)的,AuthenticationManager可以有多個(gè),他們?nèi)绻J(rèn)證失敗就會(huì)調(diào)用父親也就是全局的AuthenticationManager再去認(rèn)證看看,一般只用一個(gè)全局的.
一個(gè)AuthenticationProvider代表一種認(rèn)證方法,只要其中一個(gè)AuthenticationProvider認(rèn)證通過就算登陸成功,記住兩個(gè)主角的實(shí)現(xiàn)類ProviderManager和DaoAuthenticationProvider

回到attemptAuthentication()方法,調(diào)用拿到全局的AuthenticationMananger去執(zhí)行*authenticate()*方法,拿到ProviderManager中所有的AuthenticationProvider,交給他們?nèi)フJ(rèn)證


在遍歷provider這個(gè)過程中,調(diào)用了provider(DaoAuthenticationProvider)的authenticate方法,由provider去認(rèn)證,AuthenticationProvider的實(shí)現(xiàn)類DaoAuthenticationProvider繼承了AbstractUserDetailsAuthenticationProvider,所以自然也有父類方法的*authenticate()方法,因?yàn)闆]有重寫他,所以在源碼debug階段會(huì)進(jìn)入了他的父類的authenticate()方法,他的父類AbstractUserDetailsAuthenticationProvider實(shí)現(xiàn)了AuthenticationProvider

在provider(DaoAuthenticationProvider)的authenticate()方法中,先調(diào)用retrieveUser()通過用戶名來獲取我們存儲(chǔ)中是否有該用戶,如果有就封裝到UserDetail中,后面再拿請(qǐng)求中的密碼跟UserDetail用戶信息中的密碼進(jìn)行比較,如果沒有,密碼都不用比較了,因?yàn)橛脩舾静淮嬖?provider中有個(gè)叫UserDetailService的接口,通過用戶名可以獲取我們的用戶數(shù)據(jù)(他功能相當(dāng)于一個(gè)service層去調(diào)用dao層最終返回用戶數(shù)據(jù)),在自動(dòng)裝配中,默認(rèn)配了個(gè)基于內(nèi)存存儲(chǔ)的InMemoryUserDetailsManager,他是UserDetailService的實(shí)現(xiàn)類;

所以在使用springsecurity進(jìn)行登錄認(rèn)證的時(shí)候,除了要?jiǎng)?chuàng)建配置類進(jìn)行相關(guān)內(nèi)容的配置,還要?jiǎng)?chuàng)建UserDetailService的實(shí)現(xiàn)類用于到數(shù)據(jù)庫中查詢登錄認(rèn)證所需要的信息;
并且還要?jiǎng)?chuàng)建UserDetail的實(shí)現(xiàn)類用于封裝查詢出來的數(shù)據(jù),并把數(shù)據(jù)交給springsecurity框架拿去用于認(rèn)證


最后通過additionalAuthenticationChecks()方法進(jìn)行密碼比較


認(rèn)證失敗拋異常,認(rèn)證成功則將用戶詳細(xì)信息封裝進(jìn)Authentication返回

二、Springboot整合SpringSecurity實(shí)現(xiàn)鑒權(quán)
1、整個(gè)認(rèn)證的過程其實(shí)一直在圍繞圖中過濾鏈的綠色部分,而現(xiàn)在要說的動(dòng)態(tài)鑒權(quán)主要是圍繞其橙色部分,也就是圖上標(biāo)的:FilterSecurityInterceptor

2、想知道怎么動(dòng)態(tài)鑒權(quán)首先我們要搞明白SpringSecurity的鑒權(quán)邏輯,從上圖中我們也可以看出:一個(gè)請(qǐng)求完成了認(rèn)證,且沒有拋出異常之后就會(huì)到達(dá)FilterSecurityInterceptor所負(fù)責(zé)的鑒權(quán)部分,也就是說鑒權(quán)的入口就在FilterSecurityInterceptor。
先來看看FilterSecurityInterceptor的定義和主要方法:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
}
上文代碼可以看出FilterSecurityInterceptor是抽象類AbstractSecurityInterceptor的一個(gè)實(shí)現(xiàn)類,這個(gè)AbstractSecurityInterceptor中預(yù)先寫好了一段很重要的代碼(后面會(huì)說到)。
FilterSecurityInterceptor的主要方法是doFilter方法,請(qǐng)求過來之后會(huì)執(zhí)行這個(gè)doFilter方法,F(xiàn)ilterSecurityInterceptor的doFilter方法出奇的簡單,總共只有兩行:
- 第一行是創(chuàng)建了一個(gè)FilterInvocation對(duì)象,這個(gè)FilterInvocation對(duì)象你可以當(dāng)作它封裝了request,它的主要工作就是拿請(qǐng)求里面的信息,比如請(qǐng)求的URI和method
- 第二行就調(diào)用了自身的invoke方法,并將FilterInvocation對(duì)象傳入
所以我們主要邏輯肯定是在這個(gè)invoke方法里面了,我們來打開看看:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 進(jìn)入鑒權(quán)
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
invoke方法中只有一個(gè)if-else,一般都是不滿足if中的那三個(gè)條件的,然后執(zhí)行邏輯會(huì)來到else。
else的代碼也可以概括為兩部分:
- 調(diào)用了super.beforeInvocation(fi)。
- 調(diào)用完之后過濾器繼續(xù)往下走。
第二步可以不看,每個(gè)過濾器都有這么一步,所以我們主要看super.beforeInvocation(fi),前文我已經(jīng)說過, FilterSecurityInterceptor實(shí)現(xiàn)了抽象類AbstractSecurityInterceptor, 所以這個(gè)里super其實(shí)指的就是AbstractSecurityInterceptor, 那這段代碼其實(shí)調(diào)用了AbstractSecurityInterceptor.beforeInvocation(fi), 前文我說過AbstractSecurityInterceptor中有一段很重要的代碼就是這一段, 那我們繼續(xù)來看這個(gè)beforeInvocation(fi)方法的源碼:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try {
// 鑒權(quán)需要調(diào)用的接口
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
源碼較長,這段代碼大致可以分為三步:
拿到了一個(gè)Collection對(duì)象,這個(gè)對(duì)象是一個(gè)List,其實(shí)里面是通過我們?cè)谂渲梦募信渲玫倪^濾規(guī)則獲取到請(qǐng)求需要的角色權(quán)限。
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
拿到了Authentication,這里是調(diào)用authenticateIfRequired方法拿到了,其實(shí)里面是通過SecurityContextHolder拿到的
Authentication authenticated = authenticateIfRequired();
調(diào)用了accessDecisionManager.decide(authenticated, object, attributes),前兩步都是對(duì)decide方法做參數(shù)的準(zhǔn)備,第三步才是正式去到鑒權(quán)的邏輯,既然這里面才是真正鑒權(quán)的邏輯,那也就是說鑒權(quán)其實(shí)是accessDecisionManager在做。
// 鑒權(quán)需要調(diào)用的接口
this.accessDecisionManager.decide(authenticated, object, attributes);
AccessDecisionManager是一個(gè)接口,它聲明了三個(gè)方法,除了第一個(gè)decide()鑒權(quán)方法以外,還有兩個(gè)是輔助性的方法,其作用都是甄別 decide方法中參數(shù)的有效性。
那既然是一個(gè)接口,上文中所調(diào)用的肯定是他的實(shí)現(xiàn)類了
它主要有三個(gè)實(shí)現(xiàn)類,分別代表了三種不同的鑒權(quán)邏輯:
- AffirmativeBased:一票通過,只要有一票通過就算通過,默認(rèn)是它。
- UnanimousBased:一票反對(duì),只要有一票反對(duì)就不能通過。
- ConsensusBased:少數(shù)票服從多數(shù)票。
這里的表述為什么要用票呢?因?yàn)樵趯?shí)現(xiàn)類里面采用了委托的形式,將請(qǐng)求委托給投票器,每個(gè)投票器拿著這個(gè)請(qǐng)求根據(jù)自身的邏輯來計(jì)算出能不能通過然后進(jìn)行投票,所以會(huì)有上面的表述。
也就是說這三個(gè)實(shí)現(xiàn)類,其實(shí)還不是真正判斷請(qǐng)求能不能通過的類,真正判斷請(qǐng)求是否通過的是投票器,然后實(shí)現(xiàn)類把投票器的結(jié)果綜合起來來決定到底能不能通過。
剛剛已經(jīng)說過,實(shí)現(xiàn)類把投票器的結(jié)果綜合起來進(jìn)行決定,也就是說投票器可以放入多個(gè),每個(gè)實(shí)現(xiàn)類里的投票器數(shù)量取決于構(gòu)造的時(shí)候放入了多少投票器,我們可以看看默認(rèn)的AffirmativeBased的源碼。
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
// 拿到所有的投票器,循環(huán)遍歷進(jìn)行投票
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
AffirmativeBased的構(gòu)造是傳入投票器List,其主要鑒權(quán)邏輯交給投票器去判斷,投票器返回不同的數(shù)字代表不同的結(jié)果,然后AffirmativeBased根據(jù)自身一票通過的策略決定放行還是拋出異常。
AffirmativeBased默認(rèn)傳入的構(gòu)造器只有一個(gè)->WebExpressionVoter,這個(gè)構(gòu)造器會(huì)根據(jù)你在配置文件中的配置進(jìn)行邏輯處理得出投票結(jié)果。
所以SpringSecurity默認(rèn)的鑒權(quán)邏輯就是根據(jù)配置文件中的配置進(jìn)行鑒權(quán),這是符合我們現(xiàn)有認(rèn)知的
3、總結(jié)一下就是:
FilterSecurityInterceptor執(zhí)行doFilter 方法創(chuàng)建FilterInvocation(req,resp,chain)對(duì)象;然后調(diào)用自身invoke方法,傳入對(duì)象
invoke方法中,在 chain().doFilter 前有 super.beforeInvocation(fi),調(diào)用 AbstractSecurityInterceptor 的beforeInvocation方法
beforeInvocation方法中
- 通過調(diào)用請(qǐng)求過濾接口obtainSecurityMetadataSource() 的getAttributes()方法獲取一個(gè)Collection對(duì)象,這個(gè)對(duì)象是一個(gè)list,里面封裝了請(qǐng)求所需要的角色權(quán)限
- 調(diào)用authenticateIfRequired方法拿到Authentication對(duì)象
- 調(diào)用了accessDecisionManager.decide(authenticated, object, attributes)正式進(jìn)行鑒權(quán)
4、在使用springsecurity進(jìn)行鑒權(quán)操作的時(shí)候,根據(jù)具體業(yè)務(wù)需求去自定義請(qǐng)求過濾器obtainSecurityMetadataSource()和投票器accessDecisionManager()
- 自定義請(qǐng)求過濾器,重寫getAttributes()方法
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 修改接口角色關(guān)系后重新加載
if (CollectionUtils.isEmpty(resourceRoleList)) {
this.loadDataSource();
}
//Spring Security 通過FilterInvocation對(duì)object進(jìn)行封裝,可以安全的拿到其HttpServletRequest 和 HttpServletResponse對(duì)象
FilterInvocation fi = (FilterInvocation) object;
// 獲取用戶請(qǐng)求方式
String method = fi.getRequest().getMethod();
// 獲取用戶請(qǐng)求Url
String url = fi.getRequest().getRequestURI();
//new一個(gè)工具類AntPathMatcher的實(shí)例化對(duì)象,把路徑匹配委托給AntPathMatcher實(shí)現(xiàn)
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 獲取接口角色信息,若為匿名接口則放行,若無對(duì)應(yīng)角色則禁止
for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {
//判斷resourceRoleList中是否有和參數(shù)對(duì)象的URL和method完全相同的對(duì)象
if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) && resourceRoleDTO.getRequestMethod().equals(method)) {
//如果有對(duì)象匹配成功,則獲取該對(duì)象的角色列表RoleList
List<String> roleList = resourceRoleDTO.getRoleList();
if (CollectionUtils.isEmpty(roleList)) {
return SecurityConfig.createList("disable");
}
return SecurityConfig.createList(roleList.toArray(new String[]{})); //rolelist集合轉(zhuǎn)換成String數(shù)組,通過SecurityConfig.createList(str)對(duì)結(jié)果進(jìn)行封裝,然后return
}
}
return null;
}
自定義投票器
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
// 獲取用戶權(quán)限列表
List<String> permissionList = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
for (ConfigAttribute item : collection) {
//item.getAttribute()獲取當(dāng)前用戶訪問資源所需要的權(quán)限
//如果用戶權(quán)限列表中包含該權(quán)限,則return,否則最后會(huì)提示沒有操作權(quán)限
if (permissionList.contains(item.getAttribute())) {
return;
}
}
throw new AccessDeniedException("沒有操作權(quán)限");
}
Config文件中,調(diào)用 postProcess 方法將自定義的請(qǐng)求過濾器和投票器注冊(cè)到 Spring 容器中去
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
fsi.setSecurityMetadataSource(securityMetadataSource()); //設(shè)置請(qǐng)求攔截規(guī)則
fsi.setAccessDecisionManager(accessDecisionManager()); //設(shè)置訪問決策管理器,真正的鑒權(quán)操作在這里完成
return fsi;
}
})
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java String類型對(duì)象轉(zhuǎn)換為自定義類型對(duì)象的實(shí)現(xiàn)
本文主要介紹了java String類型對(duì)象轉(zhuǎn)換為自定義類型對(duì)象的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
java中orElse和orElseGet方法區(qū)別小結(jié)
這篇文章主要給大家介紹了關(guān)于java中orElse和orElseGet方法區(qū)別的相關(guān)資料,兩者之間的區(qū)別細(xì)微,但是卻在某些場(chǎng)景下顯的很重要,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10
Java動(dòng)態(tài)字節(jié)碼注入技術(shù)的實(shí)現(xiàn)
Java動(dòng)態(tài)字節(jié)碼注入技術(shù)是一種在運(yùn)行時(shí)修改Java字節(jié)碼的技術(shù),本文主要介紹了Java動(dòng)態(tài)字節(jié)碼注入技術(shù)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
IDEA打開項(xiàng)目不顯示或者缺失項(xiàng)目文件目錄的解決方案
這篇文章主要介紹了IDEA打開項(xiàng)目不顯示或者缺失項(xiàng)目文件目錄的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-05-05
使用Spring初始化加載InitializingBean()方法
這篇文章主要介紹了使用Spring初始化加載InitializingBean()方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
Java多線程循環(huán)柵欄CyclicBarrier正確使用方法
這篇文章主要介紹了Java多線程循環(huán)柵欄CyclicBarrier正確使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Java 判斷一個(gè)時(shí)間是否在另一個(gè)時(shí)間段內(nèi)
這篇文章主要介紹了Java 判斷一個(gè)時(shí)間是否在另一個(gè)時(shí)間段內(nèi)的相關(guān)資料,需要的朋友可以參考下2016-10-10

