spring security自定義指南

本文主要研究一下幾種自定義spring security的方式css

主要方式

  • 自定義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 {

    //......

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(encoder());
        return authProvider;
    }

    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder(11);
    }
}

自定義filter

自定義filter離不開對spring security內置filter的順序的認知:

Standard Filter Aliases and Ordering

spring security內置的各類filter順序以下:html

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 {

    private final AuthenticationManager authenticationManager;

    public DemoAuthFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String token = httpServletRequest.getHeader("app_token");
        if(StringUtils.isEmpty(token)){
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "invalid token");
            return ;
        }

        try {
            Authentication auth = authenticationManager.authenticate(new WebToken(token));
            SecurityContextHolder.getContext().setAuthentication(auth);
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (AuthenticationException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, 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來實現認證。spring

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            //......
            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        //......
    }
AuthenticationProvider經過supports方法來標識它是否可以處理這個類型的Authentication。
AnonymousAuthenticationFilter構造的是AnonymousAuthenticationToken,由AnonymousAuthenticationProvider來處理
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,其實例以下:segmentfault

能夠實現Authentication接口,或者繼承AbstractAuthenticationToken
public class WebToken extends AbstractAuthenticationToken {

    private final String token;

    public WebToken(String token) {
        super(null);
        this.token = token;
    }

    @Override
    public Object getCredentials() {
        return this.token;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

這裏就自定義一下支持這類WebToken的AuthenticationProviderapi

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來進行鑑權。session

AccessDecisionManager的幾個實現:

  • AffirmativeBased(spring security默認使用)
只要有投經過(ACCESS_GRANTED)票,則直接判爲經過。若是沒有投經過票且反對(ACCESS_DENIED)票在1個及其以上的,則直接判爲不經過。
  • ConsensusBased(少數服從多數)
經過的票數大於反對的票數則判爲經過;經過的票數小於反對的票數則判爲不經過;經過的票數和反對的票數相等,則可根據配置allowIfEqualGrantedDeniedDecisions(默認爲true)進行判斷是否經過。
  • UnanimousBased(反對票優先)
不管多少投票者投了多少經過(ACCESS_GRANTED)票,只要有反對票(ACCESS_DENIED),那都判爲不經過;若是沒有反對票且有投票者投了經過票,那麼就判爲經過.

實例

其自定義方式之一能夠參考聊聊spring security的role hierarchy,展現瞭如何自定義AccessDecisionVoter。mvc

自定義securityMetadataSource

主要是經過ObjectPostProcessor來實現自定義,具體實例可參考spring security動態配置url權限app

自定義access訪問控制

對authorizeRequests的控制,可使用permitAll,anonymous,authenticated,hasAuthority,hasRole等等ide

.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 {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/login/**","/logout/**")
                .permitAll()
                .anyRequest().access("@authService.canAccess(request,authentication)");
    }
}

這個就有點像使用spring EL表達式,實現實例以下post

@Component
public class AuthService {

    public boolean canAccess(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if(principal == null){
            return false;
        }

        if(authentication instanceof AnonymousAuthenticationToken){
            //check if this uri can be access by anonymous
            //return
        }

        Set<String> roles = authentication.getAuthorities()
                .stream()
                .map(e -> e.getAuthority())
                .collect(Collectors.toSet());
        String uri = request.getRequestURI();
        //check this uri can be access by this role

        return true;

    }
}

自定義authenticationEntryPoint

好比你想給basic認證換個realmName,除了再spring security配置中指定

security.basic.realm=myrealm

也能夠這樣

httpBasic().authenticationEntryPoint(createBasicAuthEntryPoint("myrealm"))

    public static BasicAuthenticationEntryPoint createBasicAuthEntryPoint(String realmName){
        BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint();
        entryPoint.setRealmName(realmName);
        return entryPoint;
    }

自定義多個WebSecurityConfigurerAdapter

spring security使用antMatchers不支持not的狀況,所以能夠自定義多個WebSecurityConfigurerAdapter,利用order優先級來實現匹配的覆蓋,具體能夠參考這篇文章Multiple Entry Points in Spring Security

小結

還有其餘自定義的方式,等後續有發現再補上。

doc

相關文章
相關標籤/搜索