Spring Security 實戰乾貨:內置 Filter 全解析 中提到的第 32 個 Filter
不知道你是否有印象。它決定了訪問特定路徑應該具有的權限,訪問的用戶的角色,權限是什麼?訪問的路徑須要什麼樣的角色和權限? 它就是 FilterSecurityInterceptor
,正是咱們須要的那個輪子。html
過濾器排行榜第 32 位!肩負對 http 接口權限認證的重要職責。咱們來看它的過濾邏輯:java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
複製代碼
初始化了一個 FilterInvocation
而後被 invoke
方法處理:web
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);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
複製代碼
每一次請求被 Filter
過濾都會被打上標記 FILTER_APPLIED
,沒有被打上標記的 走了父類的 beforeInvocation
方法而後再進入過濾器鏈,看上去是走了一個前置的處理。那麼前置處理了什麼呢? 首先會經過 this.obtainSecurityMetadataSource().getAttributes(Object object)
拿受保護對象(就是當前請求的 URI)全部的映射角色(ConfigAttribute
直接理解爲角色的進一步抽象) 。而後使用訪問決策管理器 AccessDecisionManager
進行投票決策來肯定是否放行。 咱們來看一下這兩個接口。spring
安全攔截器和「安全對象」模型參考:數據庫
元數據加載器 FilterInvocationSecurityMetadataSource
是 FilterSecurityInterceptor
的屬性,UML 圖以下:json
FilterInvocationSecurityMetadataSource
是一個標記接口,其抽象方法繼承自 SecurityMetadataSource``AopInfrastructureBean
。它的做用是來獲取咱們上一篇文章所描述的資源角色元數據。緩存
ConfigAttribute
支持全部的思路僅供參考,實際以你的業務爲準!安全
Collection<ConfigAttribute> getAttributes(Object object)
方法的實現:確定是獲取請求中的 URI
來和 全部的 資源配置中的 Ant Pattern
進行匹配以獲取對應的資源配置, 這裏須要將資源查詢接口查詢的資源配置封裝爲 AntPathRequestMatcher
以方便進行 Ant Match
。 這裏須要特別提一下若是你使用 Restful 風格,這裏 增刪改查 將很是方便你來對資源的管控。參考的實現:bash
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream()
.map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
.collect(Collectors.toSet());
}
複製代碼
HttpRequest
匹配到對應的資源配置後就能根據資源配置去取對應的角色集合。這些角色將交給訪問決策管理器 AccessDecisionManager
進行投票表決以決定是否放行。session
決策管理器 AccessDecisionManager
用來投票決定是否放行請求。
public interface AccessDecisionManager {
// 決策 主要經過其持有的 AccessDecisionVoter 來進行投票決策
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
// 以肯定AccessDecisionManager是否能夠處理傳遞的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//以確保配置的AccessDecisionManager支持安全攔截器將呈現的安全 object 類型。
boolean supports(Class<?> clazz);
}
複製代碼
AccessDecisionManager
有三個默認實現:
投票決策模型參考:
動態控制權限就須要咱們實現本身的訪問決策器。咱們上面說了默認有三個實現,這裏我選擇基於確定的決策器 AffirmativeBased
,只要用戶持有一個持有一個角色包含想要訪問的資源就能訪問該資源。接下來就是投票器 AccessDecisionVoter
的定義了,其實咱們能夠選擇內置的
決策投票器 AccessDecisionVoter
將安全配置屬性 ConfigAttribute
以特定的邏輯進行解析並基於特定的策略來進行投票,投同意票時總票數 +1
,反對票總票數 -1
,棄權時總票數 +0
, 而後由 AccessDecisionManager
根據具體的計票策略來決定是否放行。
Spring Security 提供的最經常使用的投票器是角色投票器 RoleVoter
,它將安全配置屬性 ConfigAttribute
視爲簡單的角色名稱,並在用戶被分配了該角色時授予訪問權限。 若是任何 ConfigAttribute
之前綴 ROLE_
開頭,它將投票。若是有一個 GrantedAuthority
返回一個字符串(經過 getAuthority()
方法)正好等於一個或多個從前綴 ROLE_
開始的 ConfigAttributes
,它將投票授予訪問權限。若是沒有任何以 ROLE_
開頭的 ConfigAttributes
匹配,則 RoleVoter
將投票拒絕訪問。若是沒有 ConfigAttribute
以 ROLE_爲前綴,將棄權。 這正是咱們想要的投票器。
一般要求應用程序中的特定角色應自動「包含」其餘角色。例如,在具備 ROLE_ADMIN
和 ROLE_USER
角色概念的應用中,您可能但願管理員可以執行普通用戶能夠執行的全部操做。你不得不進行各類複雜的邏輯嵌套來知足這一需求。如今幸虧有了 RoleHierarchyVoter
能夠幫你減小這種負擔。 它由上面的 RoleVoter
派生,經過配置了一個 RoleHierarchy
就能夠實現 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST
這種層次包含結構,左邊的必定能訪問右邊能夠訪問的資源。具體的配置規則爲:角色從左到右、從高到低以 >
相連(注意兩個空格),以換行符 \n
爲分割線。舉個例子
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
複製代碼
請注意動態配置中你須要自行實現角色分層的邏輯。DEMO 中並未對該風格進行實現。
配置須要兩個方面。
咱們須要將元數據加載器 和 訪問決策器注入 Spring IoC :
/** * 動態權限組件配置 * * @author Felordcn */
@Configuration
public class DynamicAccessControlConfiguration {
/** * RequestMatcher 生成器 * @return RequestMatcher */
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream()
.map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
.collect(Collectors.toSet());
}
/** * 元數據加載器 * * @return dynamicFilterInvocationSecurityMetadataSource */
@Bean
public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource() {
return new DynamicFilterInvocationSecurityMetadataSource();
}
/** * 角色投票器 * @return roleVoter */
@Bean
public RoleVoter roleVoter() {
return new RoleVoter();
}
/** * 基於確定的訪問決策器 * * @param decisionVoters AccessDecisionVoter類型的 Bean 會自動注入到 decisionVoters * @return affirmativeBased */
@Bean
public AccessDecisionManager affirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
return new AffirmativeBased(decisionVoters);
}
}
複製代碼
Spring Security 的 Java Configuration 不會公開它配置的每一個 object 的每一個 property。這簡化了大多數用戶的配置。 雖然有充分的理由不直接公開每一個 property,但用戶可能仍須要像本文同樣的取實現個性化需求。爲了解決這個問題,Spring Security 引入了 ObjectPostProcessor
的概念,它可用於修改或替換 Java Configuration 建立的許多 Object
實例。 FilterSecurityInterceptor
的替換配置正是經過這種方式來進行:
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
private static final String LOGIN_PROCESSING_URL = "/process";
/** * Json login post processor json login post processor. * * @return the json login post processor */
@Bean
public JsonLoginPostProcessor jsonLoginPostProcessor() {
return new JsonLoginPostProcessor();
}
/** * Pre login filter pre login filter. * * @param loginPostProcessors the login post processors * @return the pre login filter */
@Bean
public PreLoginFilter preLoginFilter(Collection<LoginPostProcessor> loginPostProcessors) {
return new PreLoginFilter(LOGIN_PROCESSING_URL, loginPostProcessors);
}
/** * Jwt 認證過濾器. * * @param jwtTokenGenerator jwt 工具類 負責 生成 驗證 解析 * @param jwtTokenStorage jwt 緩存存儲接口 * @return the jwt authentication filter */
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
return new JwtAuthenticationFilter(jwtTokenGenerator, jwtTokenStorage);
}
/** * The type Default configurer adapter. */
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private PreLoginFilter preLoginFilter;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private AccessDecisionManager accessDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
// session 生成策略用無狀態策略
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
.and()
// 動態權限配置
.authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor())
.and()
.addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
// jwt 必須配置於 UsernamePasswordAuthenticationFilter 以前
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 登陸 成功後返回jwt token 失敗後返回 錯誤信息
.formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
.and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
}
/** * 自定義 FilterSecurityInterceptor ObjectPostProcessor 以替換默認配置達到動態權限的目的 * * @return ObjectPostProcessor */
private ObjectPostProcessor<FilterSecurityInterceptor> filterSecurityInterceptorObjectPostProcessor() {
return new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return object;
}
};
}
}
}
複製代碼
而後你編寫一個 Controller
方法就將其在數據庫註冊爲一個資源進行動態的訪問控制了。無須註解或者更詳細的 Java Config 配置。
從最開始到如今一共 10 個 DEMO 。咱們按部就班地從如何學習 Spring Security 到目前實現了基於 RBAC、動態的權限資源訪問控制。若是你能堅持到如今那麼已經能知足了一些基本開發定製的須要。固然 Spring Security 還有不少局部的一些概念,我也會在之後抽時間進行講解。
我先喘口氣休幾天。後續的一些 Spring Security 教程將圍繞目前更加流行的 OAuth2.0、 SSO 、OpenID 展開。敬請關注 felord.cn
老規矩, 關注 Felordcn 回覆 day10 獲取 DEMO 。
關注公衆號:Felordcn獲取更多資訊