Spring Security之動態配置資源許可權
技術標籤:SpringBootSpringJava
在Spring Security中實現通過資料庫動態配置url資源許可權,需要通過配置驗證過濾器來實現資源許可權的載入、驗證。系統啟動時,到資料庫載入系統資源許可權列表,當有請求訪問時,通過對比系統資源許可權列表和使用者資源許可權列表(在使用者登入時新增到使用者資訊中)來判斷使用者是否有該url的訪問許可權。
在配置驗證過濾器時需要的配置項有如下幾個:
- filterSecurityInterceptor:通過繼承AbstractSecurityInterceptor並實現Filter介面自定義一個驗證過濾器,替換預設驗證過濾器。
- accessDecisionManager:通過實現AccessDecisionManager介面自定義一個決策管理器,判斷是否有訪問許可權。判斷邏輯可以寫在決策管理器的決策方法中,也可以通過投票器實現,除了框架提供的三種投票器還可以新增自定義投票器。自定義投票器通過實現AccessDecisionVoter介面來實現。
- securityMetadataSource:實現FilterInvocationSecurityMetadataSource介面,在實現類中載入資源許可權,並在filterSecurityInterceptor中注入該實現類。
- WebSecurityConfig:系統配置類,需要在配置類中配置啟用filterSecurityInterceptor。
一、配置securityMetadataSource
securityMetadataSource這裡簡單理解為資源許可權資料來源,主要維護系統的資源許可權資訊。系統啟動時,可以將許可權資源資訊從配置檔案、資料庫中載入到記憶體。我們在資料庫中維護了許可權資訊,所以這裡從資料庫中載入資源許可權,如果有需要,在系統許可權變動時可以直接反饋到記憶體。
@Service public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private PermissionMapper permissionMapper;</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 資源許可權 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">volatile</span> HashMap<String, Collection<ConfigAttribute>> urlPermMap = <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; @PostConstruct </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> init() { loadResourceDefine(); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 載入資源,初始化資源變數 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> loadResourceDefine() { urlPermMap </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> HashMap<><span style="color: rgba(0, 0, 0, 1)">; ... } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> Collection<ConfigAttribute> getAttributes(Object object) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IllegalArgumentException { FilterInvocation fi </span>=<span style="color: rgba(0, 0, 0, 1)"> (FilterInvocation) object; String url </span>=<span style="color: rgba(0, 0, 0, 1)"> fi.getRequestUrl(); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 資源許可權為空,初始化資源</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">null</span> ==<span style="color: rgba(0, 0, 0, 1)"> urlPermMap) { </span><span style="color: rgba(0, 0, 255, 1)">synchronized</span> (MyFilterInvocationSecurityMetadataSource.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">null</span> ==<span style="color: rgba(0, 0, 0, 1)"> urlPermMap) { loadResourceDefine(); } } } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> urlPermMap.get(url); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> Collection<ConfigAttribute><span style="color: rgba(0, 0, 0, 1)"> getAllConfigAttributes() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> supports(Class<?><span style="color: rgba(0, 0, 0, 1)"> clazz) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> FilterInvocation.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.isAssignableFrom(clazz); }
}
建立一個FilterInvocationSecurityMetadataSource 介面的實現類,在內部定義一個Map用來維護資源許可權資訊,bean建立的時候初始化Map。然後重寫getAttributes()方法,決策器會呼叫該方法獲取url對應的許可權。
二、配置accessDecisionManager@Service public class MyAccessDecisionManager implements AccessDecisionManager { /** * 決策方法:許可權判斷 * * @param authentication 使用者的身份資訊; * @param object 包含客戶端發起的請求的request資訊,可轉換為 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); * @param configAttributes 是MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法返回的結果, * 此方法是為了判定使用者請求的url 是否在許可權表中,如果在許可權表中,則返回給 decide 方法,用來判定使用者是否有此許可權;如果不在許可權表中則放行。 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (Collections.isEmpty(configAttributes)) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (GrantedAuthority ga : authentication.getAuthorities()) {<br></span><span style="color: rgba(0, 0, 255, 1)"> if</span><span style="color: rgba(0, 0, 0, 1)"> (configAttributes.contains(ga.getAuthority())){ </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AccessDeniedException(StatusCodeEnum.UNAUTHORIZED.getValue()); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> supports(ConfigAttribute attribute) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> supports(Class<?><span style="color: rgba(0, 0, 0, 1)"> clazz) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">; }
}
重寫AccessDecisionManager 的decide()方法,在該方法中定義具體的判斷邏輯,也可以通過定義投票器來實現。
三、配置filterSecurityInterceptor驗證過濾器的功能實際是通過依賴的資源許可權和決策管理器來實現的,參照預設的驗證過濾器FilterSecurityInterceptor來實現一個自定義驗證過濾器。
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private MyFilterInvocationSecurityMetadataSource securityMetadataSource;@Autowired </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) { </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.setAccessDecisionManager(myAccessDecisionManager); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> init(FilterConfig filterConfig) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> ServletException { } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> doFilter(ServletRequest request, ServletResponse response, FilterChain chain) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException, ServletException { FilterInvocation fi </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> FilterInvocation(request, response, chain); invoke(fi); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> fi 裡面有一個被攔截的url,呼叫MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有許可權, * 再呼叫MyAccessDecisionManager的decide方法來校驗使用者的許可權是否足夠 * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> IOException * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> ServletException </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> invoke(FilterInvocation fi) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException, ServletException { InterceptorStatusToken token </span>= <span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.beforeInvocation(fi); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 執行下一個攔截器</span>
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> destroy() {
}
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> Class<?><span style="color: rgba(0, 0, 0, 1)"> getSecureObjectClass() {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> FilterInvocation.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">;
}
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> SecurityMetadataSource obtainSecurityMetadataSource() {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.securityMetadataSource;
}
}
這裡建立一個自定義驗證過濾器,然後將前面定義的MyFilterInvocationSecurityMetadataSource 和MyAccessDecisionManager 配置進來。最後還需要在系統配置檔案中啟用該驗證過濾器。
四、配置WebSecurityConfig@Configuration @EnableWebSecurity // 禁用Spring Boot預設的Security配置,配合@Configuration啟用自定義配置 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService;
// 自定義決策管理器 @Autowired private MyAccessDecisionManager myAccessDecisionManager;</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> * 加密工具 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Bean </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> PasswordEncoder passwordEncoder() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> BCryptPasswordEncoder(); } </span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> * 認證管理器 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Override @Bean </span><span style="color: rgba(0, 0, 255, 1)">public</span> AuthenticationManager authenticationManagerBean() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.authenticationManagerBean(); } </span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> * 身份驗證配置,用於注入自定義身份驗證Bean和密碼校驗規則 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Override </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(AuthenticationManagerBuilder auth) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 無需許可權校驗直接放行的路徑 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String[] PATH_PASS =<span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 根據實際情況新增</span>
};
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
* Request層面的配置,對應XML Configuration中的<http>元素
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@Override
</span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(HttpSecurity http) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception {
http
.authorizeRequests()
.antMatchers(PATH_PASS).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.httpBasic();
</span><span style="color: rgba(238, 0, 0, 1)">//</span><span style="color: rgba(238, 0, 0, 1)"> 將自定義的過濾器配置在FilterSecurityInterceptor之前</span>
http.addFilterBefore(myFilterSecurityInterceptor(), FilterSecurityInterceptor.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
}
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
* Web層面的配置,一般用來配置無需許可權校驗的路徑,也可以在HttpSecurity中配置,但是在web.ignoring()中配置效率更高。
* web.ignoring()是一個忽略的過濾器,而HttpSecurity中定義了一個過濾器鏈,即使permitAll()放行還是會走所有的過濾器,
* 直到最後一個過濾器FilterSecurityInterceptor認定是可以放行的,才能訪問。
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(WebSecurity web) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception {
web.ignoring().antMatchers(</span>"/favor.ioc"<span style="color: rgba(0, 0, 0, 1)">);
}
</span><span style="color: rgba(238, 0, 0, 1)">/**</span><span style="color: rgba(238, 0, 0, 1)">
* 管理自定義的許可權過濾器
</span><span style="color: rgba(238, 0, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> MyFilterSecurityInterceptor myFilterSecurityInterceptor() {
MyFilterSecurityInterceptor myFilterSecurityInterceptor </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MyFilterSecurityInterceptor();
myFilterSecurityInterceptor.setMyAccessDecisionManager(myAccessDecisionManager);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> myFilterSecurityInterceptor;
}
}
程式碼中註釋標紅的部分為配置自定義驗證過濾器需要注意的地方。
* 這裡是在授權伺服器中實現的動態配置資源許可權,在資源伺服器中方法一樣。