本文主要研究一下幾種自定義spring security的方式css
@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,能夠自行擴展html
自定義密碼的加密方式,實例以下spring
@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離不開對spring security內置filter的順序的認知:segmentfault
spring security內置的各類filter順序以下:api
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 |
參數有username,password的,走UsernamePasswordAuthenticationFilter,提取參數構造UsernamePasswordAuthenticationToken進行認證,成功則填充SecurityContextHolder的Authenticationbash
header裏頭有Authorization,並且value是以Basic開頭的,則走BasicAuthenticationFilter,提取參數構造UsernamePasswordAuthenticationToken進行認證,成功則填充SecurityContextHolder的Authenticationsession
給沒有登錄的用戶,填充AnonymousAuthenticationToken到SecurityContextHolder的Authenticationmvc
能夠像UsernamePasswordAuthenticationFilter或者AnonymousAuthenticationFilter繼承GenericFilterBean,或者像BasicAuthenticationFilter繼承OncePerRequestFilter。 關於GenericFilterBean與OncePerRequestFilter的區別能夠見這篇spring mvc中的幾類攔截器對比app
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以後,而後就要將它放置到filterChain中ide
@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);
複製代碼
AuthenticationManager接口有個實現ProviderManager至關於一個provider chain,它裏頭有個List providers,經過provider來實現認證。
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,其實例以下:
能夠實現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的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));
}
}
複製代碼
前面有filter處理了登陸問題,接下來是否可訪問指定資源的問題就由FilterSecurityInterceptor來處理了。而FilterSecurityInterceptor是用了AccessDecisionManager來進行鑑權。
spring security默認使用
)只要有投經過(ACCESS_GRANTED)票,則直接判爲經過。若是沒有投經過票且反對(ACCESS_DENIED)票在1個及其以上的,則直接判爲不經過。
少數服從多數
)經過的票數大於反對的票數則判爲經過;經過的票數小於反對的票數則判爲不經過;經過的票數和反對的票數相等,則可根據配置allowIfEqualGrantedDeniedDecisions(默認爲true)進行判斷是否經過。
反對票優先
)不管多少投票者投了多少經過(ACCESS_GRANTED)票,只要有反對票(ACCESS_DENIED),那都判爲不經過;若是沒有反對票且有投票者投了經過票,那麼就判爲經過.
其自定義方式之一能夠參考聊聊spring security的role hierarchy,展現瞭如何自定義AccessDecisionVoter。
主要是經過ObjectPostProcessor來實現自定義,具體實例可參考spring security動態配置url權限
對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 {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login/**","/logout/**")
.permitAll()
.anyRequest().access("@authService.canAccess(request,authentication)");
}
}
複製代碼
這個就有點像使用spring EL表達式,實現實例以下
@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;
}
}
複製代碼
好比你想給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;
}
複製代碼
spring security使用antMatchers不支持not的狀況,所以能夠自定義多個WebSecurityConfigurerAdapter,利用order優先級來實現匹配的覆蓋,具體能夠參考這篇文章Multiple Entry Points in Spring Security
還有其餘自定義的方式,等後續有發現再補上。