亞寵展、全球寵物產業風向標——亞洲寵物展覽會深度解析
809
2022-05-30
在業務系統中很可能遇到兩個或者用戶體系,比如后臺管理用戶和前臺APP用戶。很多時候這兩種用戶走的還是兩種不同的體系,比如后臺用戶用有狀態的Session,而前臺用戶用流行的無狀態JWT,總之它們是兩種完全不同的隔離體系。這種需求該怎么實現呢?其中有哪些坑要踩呢?本文將告訴你怎么做。
路徑攔截策略
在Spring Security中當然是按照不同的請求路徑規則定義專門的過濾器鏈,你可以通過三種方式來實現路徑攔截。然后按照策略定義過濾器鏈即可:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE?+?1)
SecurityFilterChain?systemSecurityFilterChain(HttpSecurity?http)?throws?Exception?{
//?省略
}
這三種策略介紹如下。
按照正則過濾
你可以通過HttpSecurity提供的過濾器過濾URI,例如攔截請求中在query參數而且包含id的URI:
http.regexMatcher("/(\\?|\\&)\"?+?id?+?\"=([^\\&]+)/")
這種常用來匹配一些帶參數的URL。
按照Ant規則過濾
這種是我們常見的方式,例如攔截/system開頭的所有路徑:
http.antMatcher("/system/**")
關于這種方式這里不再贅述,詳細可以通過Ant規則詳解這一篇來了解。
按照RequestMatcher過濾
一些復雜的組合可以通過定義RequestMatcher接口來組合,例如這種復雜的規則:
RequestMatcher?requestMatcher?=?new?OrRequestMatcher(
new?AntPathRequestMatcher(
providerSettings.getTokenEndpoint(),
HttpMethod.POST.name()),
new?AntPathRequestMatcher(
providerSettings.getTokenIntrospectionEndpoint(),
HttpMethod.POST.name()),
new?AntPathRequestMatcher(
providerSettings.getTokenRevocationEndpoint(),
HttpMethod.POST.name()));
http.requestMatcher(requestMatcher)
滿足三個路徑中的一個就行,這種組合方式能夠實現最復雜的攔截策略。
配置隔離的一些要點
這里還要注意配置之間的隔離。
Session會話
默認情況下的Session依賴于cookie中設定的jsessionid, 如果你使用會話模式,必須隔離多個過濾器鏈的會話存儲,這樣能夠實現一個多個過濾器在同一個會話下不同的登錄狀態,否則它們共享配置就會發生錯亂。
這是因為在一個會話下,默認的屬性Key是SPRING_SECURITY_CONTEXT,當在同一個會話下(同一個瀏覽器不同的tab頁)獲取當前上下文都是這樣的:
//?默認?SPRING_SECURITY_CONTEXT
Object?contextFromSession?=?httpSession.getAttribute(this.springSecurityContextKey);
這樣登錄一個,其它都認為是登錄狀態,這顯然不符合預期。你需要在不同的過濾器中定義不同的會話屬性Key。
final?String?ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY?="SOME_UNIQUE_KEY"
HttpSessionSecurityContextRepository?hs?=?new?HttpSessionSecurityContextRepository();
hs.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
http.securityContext().securityContextRepository(hs)
無狀態Token
無狀態Token相對簡單一些,前端根據路徑分開存儲即可,而且Token中應該包含校驗過濾器鏈的信息以方便后端校驗,避免Token混用。
UserDetailsService
如果你的不同端的用戶是獨立的,你需要實現不同的UserDetailsService,但是存在多個UserDetailsService的話,
一定不要將它們直接注冊到Spring IoC中!
一定不要將它們直接注冊到Spring IoC中!
一定不要將它們直接注冊到Spring IoC中!
如果你一定要注冊到Spring IoC,你需要定義獨立的接口,就像這樣:
@FunctionalInterface
public?interface?OAuth2UserDetailsService?{
UserDetails?loadOAuth2UserByUsername(String?username)?throws?UsernameNotFoundException;
}
然后實現該接口再注入Spring IoC,每個過濾器鏈配置的時候就可以這樣寫:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE?+?2)
SecurityFilterChain?defaultSecurityFilterChain(HttpSecurity?http,
OAuth2UserDetailsService?oAuth2UserDetailsService)?throws?Exception?{
http.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
}
但是Spring IoC中必須有一個UserDetailsService,你得這樣寫:
@Bean
UserDetailsService notFoundUserDetailsService() {
return username -> {
throw new UsernameNotFoundException("用戶未找到");
};
}
為啥不可用,因為注入Spring IoC的UserDetailsService是一個兜底的實現,如果你只有一個實現,放入Spring IoC無可厚非,如果你想讓多個各自走各自的就必須這樣寫最安全,不然還有一個默認的InMemoryUserDetailsManager也會生效成為兜底的。
其它
其它配置按照各自的配置就行了,目前我還沒有發現有沖突的地方。上面所講的東西,在Id Server授權服務器中就是這樣實現授權服務器過濾、后臺管理用戶和前臺授權用戶三者之間隔離的:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled?=?true)
public?class?IdServerSecurityConfiguration?{
private?static?final?String?CUSTOM_CONSENT_PAGE_URI?=?"/oauth2/consent";
private?static?final?String?SYSTEM_ANT_PATH?=?"/system/**";
/**
*?The?constant?ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY.
*/
public?static?final?String?ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY?=?"ID_SERVER_SYSTEM_SECURITY_CONTEXT";
/**
*?授權服務器配置
*
*?@author?felord.cn
*?@since?1.0.0
*/
@Configuration(proxyBeanMethods?=?false)
public?static?class?AuthorizationServerConfiguration?{
/**
*?Authorization?server?集成?優先級要高一些
*
*?@param?http?the?http
*?@return?the?security?filter?chain
*?@throws?Exception?the?exception
*?@since?1.0.0
*/
@Bean("authorizationServerSecurityFilterChain")
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain?authorizationServerSecurityFilterChain(HttpSecurity?http)?throws?Exception?{
OAuth2AuthorizationServerConfigurer
new?OAuth2AuthorizationServerConfigurer<>();
//??把自定義的授權確認URI加入配置
authorizationServerConfigurer.authorizationEndpoint(authorizationEndpointConfigurer?->
authorizationEndpointConfigurer.consentPage(CUSTOM_CONSENT_PAGE_URI));
RequestMatcher?authorizationServerEndpointsMatcher?=?authorizationServerConfigurer.getEndpointsMatcher();
//?攔截?授權服務器相關的請求端點
http.requestMatcher(authorizationServerEndpointsMatcher)
.authorizeRequests().anyRequest().authenticated()
.and()
//?忽略掉相關端點的csrf
.csrf(csrf?->?csrf
.ignoringRequestMatchers(authorizationServerEndpointsMatcher))
.formLogin()
.and()
//?應用?授權服務器的配置
.apply(authorizationServerConfigurer);
return?http.build();
}
/**
*?配置?OAuth2.0?provider元信息
*
*?@param?port?the?port
*?@return?the?provider?settings
*?@since?1.0.0
*/
@Bean
public?ProviderSettings?providerSettings(@Value("${server.port}")?Integer?port)?{
//TODO?配置化?生產應該使用域名
return?ProviderSettings.builder().issuer("http://localhost:"?+?port).build();
}
}
/**
*?后臺安全配置.
*
*?@author?felord.cn
*?@since?1.0.0
*/
@Configuration(proxyBeanMethods?=?false)
public?static?class?SystemSecurityConfiguration?{
/**
*?管理后臺以{@code?/system}開頭
*
*?@param?http?the?http
*?@return?the?security?filter?chain
*?@throws?Exception?the?exception
*?@see?AuthorizationServerConfiguration
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE?+?1)
SecurityFilterChain?systemSecurityFilterChain(HttpSecurity?http,?UserInfoService?userInfoService)?throws?Exception?{
SimpleAuthenticationEntryPoint?authenticationEntryPoint?=?new?SimpleAuthenticationEntryPoint();
AuthenticationEntryPointFailureHandler?authenticationFailureHandler?=?new?AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
HttpSessionSecurityContextRepository?securityContextRepository?=?new?HttpSessionSecurityContextRepository();
securityContextRepository.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
http.antMatcher(SYSTEM_ANT_PATH).csrf().disable()
.headers().frameOptions().sameOrigin()
.and()
.securityContext().securityContextRepository(securityContextRepository)
.and()
.authorizeRequests().anyRequest().authenticated()
/*??.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)*/
.and()
.userDetailsService(userInfoService::findByUsername)
.formLogin().loginPage("/system/login").loginProcessingUrl("/system/login")
.successHandler(new?RedirectLoginAuthenticationSuccessHandler("/system"))
.failureHandler(authenticationFailureHandler).permitAll();
return?http.build();
}
}
/**
*?普通用戶訪問安全配置.
*
*?@author?felord.cn
*?@since?1.0.0
*/
@Configuration(proxyBeanMethods?=?false)
public?static?class?OAuth2SecurityConfiguration?{
/**
*?Default?security?filter?chain?security?filter?chain.
*
*?@param?http?????????????????????the?http
*?@param?oAuth2UserDetailsService?the?oauth2?user?details?service
*?@param?securityFilterChain??????the?security?filter?chain
*?@return?the?security?filter?chain
*?@throws?Exception?the?exception
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE?+?2)
SecurityFilterChain?defaultSecurityFilterChain(HttpSecurity?http,
OAuth2UserDetailsService?oAuth2UserDetailsService,
@Qualifier("authorizationServerSecurityFilterChain")?SecurityFilterChain?securityFilterChain)?throws?Exception?{
DefaultSecurityFilterChain?authorizationServerFilterChain?=?(DefaultSecurityFilterChain)?securityFilterChain;
SimpleAuthenticationEntryPoint?authenticationEntryPoint?=?new?SimpleAuthenticationEntryPoint();
AuthenticationEntryPointFailureHandler?authenticationFailureHandler?=?new?AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
http.requestMatcher(new?AndRequestMatcher(
new?NegatedRequestMatcher(new?AntPathRequestMatcher(SYSTEM_ANT_PATH)),
new?NegatedRequestMatcher(authorizationServerFilterChain.getRequestMatcher())
)).authorizeRequests(authorizeRequests?->
authorizeRequests
.anyRequest().authenticated()
).csrf().disable()
.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
.formLogin().loginPage("/login")
.successHandler(new?RedirectLoginAuthenticationSuccessHandler())
.failureHandler(authenticationFailureHandler).permitAll()
.and()
.oauth2ResourceServer().jwt();
return?http.build();
}
}
}
你可以通過https://github.com/NotFound403/id-server下載源碼進行改造學習,歡迎Star。
OAuth2教程可通過https://blog.csdn.net/qq_35067322/category_11691173.html訂閱。
Spring 網絡
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。