序
本文主要研究一下幾種自定義spring security的方式
主要方式
- 自定義UserDetailsService
- 自定義passwordEncoder
- 自定義filter
- 自定義AuthenticationProvider
- 自定義AccessDecisionManager
- 自定義securityMetadataSource
- 自定義access訪問控制
- 自定義authenticationEntryPoint
- 自定義多個WebSecurityConfigurerAdapter
自定義UserDetailsService
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......
@Bean
@Override
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("demoUser1").password("123456")
.authorities("ROLE_USER","read_x").build());
manager.createUser(User.withUsername("admin").password("123456")
.authorities("ROLE_ADMIN").build());
return manager;
}
}
通過重寫userDetailsService()方法自定義userDetailsService。這里展示的是InMemoryUserDetailsManager。
spring security內置了JdbcUserDetailsManager,可以自行擴展
自定義passwordEncoder
自定義密碼的加密方式,實例如下
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { <span class="hljs-comment">//......</span>
<span class="hljs-meta">@Bean</span>
public <span class="hljs-type">DaoAuthenticationProvider</span> authenticationProvider() {
<span class="hljs-keyword">final</span> <span class="hljs-type">DaoAuthenticationProvider</span> authProvider = <span class="hljs-keyword">new</span> <span class="hljs-type">DaoAuthenticationProvider</span>();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
<span class="hljs-keyword">return</span> authProvider;
}
<span class="hljs-meta">@Bean</span>
public <span class="hljs-type">PasswordEncoder</span> encoder() {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-type">BCryptPasswordEncoder</span>(<span class="hljs-number">11</span>);
}
}
自定義filter
自定義filter離不開對spring security內置filter的順序的認知:
Standard Filter Aliases and Ordering
spring security內置的各種filter順序如下:
Alias | Filter Class | Namespace Element or Attribute |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
HEADERS_FILTER | HeaderWriterFilter | http/headers |
CSRF_FILTER | CsrfFilter | http/csrf |
LOGOUT_FILTER | LogoutFilter | http/logout |
X509_FILTER | X509AuthenticationFilter | http/x509 |
PRE_AUTH_FILTER | AbstractPreAuthenticatedProcessingFilter Subclasses | N/A |
CAS_FILTER | CasAuthenticationFilter | N/A |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http/@servlet-api-provision |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http/@jaas-api-provision |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
SWITCH_USER_FILTER | SwitchUserFilter | N/A |
內置的認證filter
- UsernamePasswordAuthenticationFilter
參數有username,password的,走UsernamePasswordAuthenticationFilter,提取參數構造UsernamePasswordAuthenticationToken進行認證,成功則填充SecurityContextHolder的Authentication
- BasicAuthenticationFilter
header里頭有Authorization,而且value是以Basic開頭的,則走BasicAuthenticationFilter,提取參數構造UsernamePasswordAuthenticationToken進行認證,成功則填充SecurityContextHolder的Authentication
- AnonymousAuthenticationFilter
給沒有登陸的用戶,填充AnonymousAuthenticationToken到SecurityContextHolder的Authentication
定義自己的filter
可以像UsernamePasswordAuthenticationFilter或者AnonymousAuthenticationFilter繼承GenericFilterBean,或者像BasicAuthenticationFilter繼承OncePerRequestFilter。
關於GenericFilterBean與OncePerRequestFilter的區別可以見這篇 spring mvc中的幾類攔截器對比
自定義filter主要完成功能如下:
- 提取認證參數
- 調用認證,成功則填充SecurityContextHolder的Authentication,失敗則拋出異常
實例
public class DemoAuthFilter extends GenericFilterBean { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">AuthenticationManager</span> authenticationManager;
public <span class="hljs-type">DemoAuthFilter</span>(<span class="hljs-type">AuthenticationManager</span> authenticationManager) {
<span class="hljs-keyword">this</span>.authenticationManager = authenticationManager;
}
<span class="hljs-meta">@Override</span>
public void doFilter(<span class="hljs-type">ServletRequest</span> servletRequest, <span class="hljs-type">ServletResponse</span> servletResponse, <span class="hljs-type">FilterChain</span> filterChain) <span class="hljs-keyword">throws</span> <span class="hljs-type">IOException</span>, <span class="hljs-type">ServletException</span> {
<span class="hljs-type">HttpServletRequest</span> httpServletRequest = (<span class="hljs-type">HttpServletRequest</span>) servletRequest;
<span class="hljs-type">HttpServletResponse</span> httpServletResponse = (<span class="hljs-type">HttpServletResponse</span>) servletResponse;
<span class="hljs-type">String</span> token = httpServletRequest.getHeader(<span class="hljs-string">"app_token"</span>);
<span class="hljs-keyword">if</span>(<span class="hljs-type">StringUtils</span>.isEmpty(token)){
httpServletResponse.sendError(<span class="hljs-type">HttpServletResponse</span>.<span class="hljs-type">SC_UNAUTHORIZED</span>, <span class="hljs-string">"invalid token"</span>);
<span class="hljs-keyword">return</span> ;
}
<span class="hljs-keyword">try</span> {
<span class="hljs-type">Authentication</span> auth = authenticationManager.authenticate(<span class="hljs-keyword">new</span> <span class="hljs-type">WebToken</span>(token));
<span class="hljs-type">SecurityContextHolder</span>.getContext().setAuthentication(auth);
filterChain.doFilter(servletRequest, servletResponse);
} <span class="hljs-keyword">catch</span> (<span class="hljs-type">AuthenticationException</span> e) {
httpServletResponse.sendError(<span class="hljs-type">HttpServletResponse</span>.<span class="hljs-type">SC_UNAUTHORIZED</span>, e.getMessage());
}
}
}
設置filter順序
上面定義完filter之后,然后就要將它放置到filterChain中
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new DemoAuthFilter(authenticationManager()), BasicAuthenticationFilter.class);
http.csrf().disable();
http.logout().disable();
http.sessionManagement().disable();
}
}
這里把他添加在BasicAuthenticationFilter之前,當然可以根據情況直接替換UsernamePasswordAuthenticationFilter
http.addFilterAt(new DemoAuthFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class);
自定義AuthenticationProvider
AuthenticationManager接口有個實現ProviderManager相當於一個provider chain,它里頭有個List<AuthenticationProvider> providers,通過provider來實現認證。
public class AnonymousAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
//......
public boolean supports(Class<?> authentication) {
return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
}
}
UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter構造的是UsernamePasswordAuthenticationToken,由DaoAuthenticationProvider(其父類為AbstractUserDetailsAuthenticationProvider)來處理
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
//......
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
像上面我們自定義了WebToken,其實例如下:
可以實現Authentication接口,或者繼承AbstractAuthenticationToken
public class WebToken extends AbstractAuthenticationToken { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> token;
public <span class="hljs-type">WebToken</span>(<span class="hljs-type">String</span> token) {
<span class="hljs-keyword">super</span>(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">this</span>.token = token;
}
<span class="hljs-meta">@Override</span>
public <span class="hljs-type">Object</span> getCredentials() {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.token;
}
<span class="hljs-meta">@Override</span>
public <span class="hljs-type">Object</span> getPrincipal() {
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
}
這里就自定義一下支持這類WebToken的AuthenticationProvider
AuthenticationProvider要實現的功能就是根據參數來校驗是否可以登錄通過,不通過則拋出異常;通過則獲取其GrantedAuthority填充到authentication中
如果是繼承了AbstractAuthenticationToken,則是填充其authorities屬性
前面自定義的DemoAuthFilter會在登陸成功之后,將authentication寫入到SecurityContextHolder的context中
可以實現AuthenticationProvider接口,或者繼承AbstractUserDetailsAuthenticationProvider(默認集成了preAuthenticationChecks以及postAuthenticationChecks
)
@Service
public class MyAuthProvider implements AuthenticationProvider {
//...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//......
}
@Override
public boolean supports(Class<?> authenticationClass) {
return return (WebToken.class
.isAssignableFrom(authenticationClass));
}
}
自定義AccessDecisionManager
前面有filter處理了登錄問題,接下來是否可訪問指定資源的問題就由FilterSecurityInterceptor來處理了。而FilterSecurityInterceptor是用了AccessDecisionManager來進行鑒權。
AccessDecisionManager的幾個實現:
- AffirmativeBased(
spring security默認使用
)
只要有投通過(ACCESS_GRANTED)票,則直接判為通過。如果沒有投通過票且反對(ACCESS_DENIED)票在1個及其以上的,則直接判為不通過。
- ConsensusBased(
少數服從多數
)
通過的票數大於反對的票數則判為通過;通過的票數小於反對的票數則判為不通過;通過的票數和反對的票數相等,則可根據配置allowIfEqualGrantedDeniedDecisions(默認為true)進行判斷是否通過。
- UnanimousBased(
反對票優先
)
無論多少投票者投了多少通過(ACCESS_GRANTED)票,只要有反對票(ACCESS_DENIED),那都判為不通過;如果沒有反對票且有投票者投了通過票,那么就判為通過.
實例
其自定義方式之一可以參考聊聊spring security的role hierarchy,展示了如何自定義AccessDecisionVoter。
自定義securityMetadataSource
主要是通過ObjectPostProcessor來實現自定義,具體實例可參考spring security動態配置url權限
自定義access訪問控制
對authorizeRequests的控制,可以使用permitAll,anonymous,authenticated,hasAuthority,hasRole等等
.antMatchers("/login","/css/**", "/js/**","/fonts/**","/file/**").permitAll()
.antMatchers("/anonymous*").anonymous()
.antMatchers("/session").authenticated()
.antMatchers("/login/impersonate").hasAuthority("ROLE_ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/auth/*").hasAnyRole("ADMIN","USER")
這些都是利用spring security內置的表達式。像hasAuthority等,他們內部還是使用access方法來實現的。因此我們也可以直接使用access,來實現最大限度的自定義。
實例
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { <span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> void configure(<span class="hljs-type">HttpSecurity</span> http) <span class="hljs-keyword">throws</span> <span class="hljs-type">Exception</span> {
http
.authorizeRequests()
.antMatchers(<span class="hljs-string">"/login/**"</span>,<span class="hljs-string">"/logout/**"</span>)
.permitAll()
.anyRequest().access(<span class="hljs-string">"@authService.canAccess(request,authentication)"</span>);
}
}
這個就有點像使用spring EL表達式,實現實例如下
@Component
public class AuthService { <span class="hljs-keyword">public</span> <span class="hljs-built_in">boolean</span> canAccess(HttpServletRequest request, Authentication authentication) {
<span class="hljs-built_in">Object</span> principal = authentication.getPrincipal();
<span class="hljs-keyword">if</span>(principal == <span class="hljs-literal">null</span>){
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}
<span class="hljs-keyword">if</span>(authentication <span class="hljs-keyword">instanceof</span> AnonymousAuthenticationToken){
<span class="hljs-comment">//check if this uri can be access by anonymous</span>
<span class="hljs-comment">//return</span>
}
Set<<span class="hljs-built_in">String</span>> roles = authentication.getAuthorities()
.stream()
.map(e -> e.getAuthority())
.collect(Collectors.toSet());
<span class="hljs-built_in">String</span> uri = request.getRequestURI();
<span class="hljs-comment">//check this uri can be access by this role</span>
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
}
自定義authenticationEntryPoint
比如你想給basic認證換個realmName,除了再spring security配置中指定
security.basic.realm=myrealm
也可以這樣
spring security使用antMatchers不支持not的情況,因此可以自定義多個WebSecurityConfigurerAdapter,利用order優先級來實現匹配的覆蓋,具體可以參考這篇文章Multiple Entry Points in Spring Security
小結
還有其他自定義的方式,等后續有發現再補上。
doc
原文地址:https://segmentfault.com/a/1190000012560773Powered by .NET Core on Kubernetes