简介 Spring security 总体分为认证和鉴权两部分,认证我们可以理解为对账号密码的验证,鉴权为用户能不能访问资源,我们可以通过实现Spring security的接口来实现自定义的认证和鉴权。Spring security的本质是一连串的拦截器,我们可以拦截器链中加入自定义的拦截器来实现自己的逻辑.
认证部分 下面介绍认证的核心接口和概念
Authentication 此接口负者存储要认证的具体信息,主要是将认证的账号密码还有权限等信息存放在其中,比如前端传入账号和密码给后端验证,那么需要将账号和密码封装进实现Authentication接口的认证类中,然后将认证信息传给Spring security拦截器链,由Spring security调用认证和鉴权方法,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface Authentication extends Principal, Serializable { //此账号具有哪些角色或者权限 Collection<? extends GrantedAuthority> getAuthorities(); //认证信息,可以理解为密码 Object getCredentials(); Object getDetails(); //认证主体,如账号 Object getPrincipal(); //这个Authentication是否已经认证过 boolean isAuthenticated(); //设置Authentication是否已经认证过 void setAuthenticated(boolean var1) throws IllegalArgumentException; }
AuthenticationManager 在框架中AuthenticationManager负责认证,一般我们使用Spring security自带实现类ProviderManager
1 2 3 4 public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException; }
ProviderManager的authenticate(Authentication var1)方法的实现,我们可以看到起主要逻辑是循环调用List providers 中的AuthenticationProvider下的authenticate(Authentication authentication)方法,如果其中有一个AuthenticationProvider认证成功则返回,那么这里认证的逻辑就已经大概清晰,Spring Security 依托ProviderManager的实现去完成认证,而ProviderManager的主要实现类是ProviderManager,ProviderManager中又有一个AuthenticationProvider对象的集合,其负责具体的验证逻辑,如果有一个认证通过,则之前存在Authentication中的信息就认证成功,我们要做的就是实现自己的AuthenticationProvider认证,将其加入到ProviderManager的认证集合List providers中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 private List<AuthenticationProvider> providers; //...省略其余代码 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); Iterator var8 = this.getProviders().iterator(); while(var8.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var8.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (InternalAuthenticationServiceException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } if (result == null && this.parent != null) { try { result = parentResult = this.parent.authenticate(authentication); } catch (ProviderNotFoundException var11) { ; } catch (AuthenticationException var12) { parentException = var12; lastException = var12; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } }
AuthenticationProvider 上文提到了AuthenticationProvider是负责具体认证的地方,它的代码如下authenticate方法负责接收一个Authentication类,如果认证成功则返回一个Authentication对象,supports(Class<?> var1)负责判定这个AuthenticationProvider可以鉴定什么类型的Authentication,我们可以实现自己的Authentication,和AuthenticationProvider,从而使我们自定义的AuthenticationProvider只去鉴定特定的Authentication
1 2 3 4 5 public interface AuthenticationProvider { Authentication authenticate(Authentication var1) throws AuthenticationException; boolean supports(Class<?> var1); }
UserDetailsService UserDetailsService是Spring security抽象的接口其方法如下,其作用是根据参数获取一个用户属性UserDetails,UserDetails为Spring secur为用户做的一个抽象。
1 2 3 public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
在网上我们看到的很多教程叫我们只要自定义实现自己的 UserDetailsService就可以完成认证,这是因为Spring security默认使用的Authentication 是UsernamePasswordAuthenticationToken,而默认对UsernamePasswordAuthenticationToken进行认证的又是AuthenticationProvider 是DaoAuthenticationProvider,下面我们查看DaoAuthenticationProvider源码发现以下代码,其中this.getUserDetailsService().loadUserByUsername就是调用UserDetailsService去获取UserDetailsService对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } }
那retrieveUser又具体做了什么呢,我们查看DaoAuthenticationProvider父类AbstractUserDetailsAuthenticationProvider中的代码,其中以下最重要的一段代码this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);这段代码的实现在DaoAuthenticationProvider中,且public boolean supports(Class<?> authentication) 表明了其只验证实现了UsernamePasswordAuthenticationToken的Authentication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("User '" + username + "' not found"); if (this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); } public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); }
DaoAuthenticationProvider中我们找到additionalAuthenticationChecks方法,这里的逻辑就很清晰了,将UserDetails中的值和authentication中的值做比对,如果不匹配抛出AuthenticationException异常由spring security去处理异常
1 2 3 4 5 6 7 8 9 10 11 12 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
拦截器 上文提到我们登陆的时候需要把我们登陆的信息封装成Authentication对象,再由Spring security框架去认证,前文还说到Spring Security的本质就是一连串的拦截器,那我们实现的思路就是这样,我们自定义一个拦截器,拦截登陆的信息,将账号密码组成Authentication对象,再交由Spring security对象去认证即可,之后以我们可以不自定义上述的类就可以使用是因为Spring security中默认加入了一个UsernamePasswordAuthenticationFilter拦截器,且提供了默认实现的DaoAuthenticationProvider.我们查看UsernamePasswordAuthenticationFilter的源码,发现其只是将前传送进来的账号密码组成一个UsernamePasswordAuthenticationToken ,我们注意到this.getAuthenticationManager().authenticate(authRequest)这段代码,这段代码就是调用ProviderManager去完成对Authentication的认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
下面我们再查看UsernamePasswordAuthenticationToken的父类AbstractAuthenticationProcessingFilter,发现其调用了UsernamePasswordAuthenticationToken的attemptAuthentication方法,并且将UsernamePasswordAuthenticationToken放入SecurityContextHolder中的SecurityContext里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = this.attemptAuthentication(request, response); if (authResult == null) { return; } this.sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); return; } if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authResult); } } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
默认的SecurityContext是由ThredLocal生成,也就是说在同一个线程下我们都可以通过SecurityContextHolder.getContext().getAuthentication()获取到Authentication或者通过 SecurityContextHolder.getContext().setAuthentication()设置Authentication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal(); ThreadLocalSecurityContextHolderStrategy() { } public void clearContext() { contextHolder.remove(); } public SecurityContext getContext() { SecurityContext ctx = (SecurityContext)contextHolder.get(); if (ctx == null) { ctx = this.createEmptyContext(); contextHolder.set(ctx); } return ctx; } public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder.set(context); } public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
在本人的自行实现的拦截器中并没有调用this.getAuthenticationManager().authenticate(authRequest)方法,因为本人只实现了OncePerRequestFilter类,并没有实现Spring security提供的AbstractAuthenticationProcessingFilter抽象类,但是仍然可以实现认证,因为在后面的鉴权拦截器中我实现了AbstractSecurityInterceptor类该中有一段代码Authentication authenticated = this.authenticateIfRequired();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); boolean debug = this.logger.isDebugEnabled(); if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass()); } else { Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); if (attributes != null && !attributes.isEmpty()) { if (debug) { this.logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = this.authenticateIfRequired(); try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException var7) { this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7)); throw var7; } if (debug) { this.logger.debug("Authorization successful"); } if (this.publishAuthorizationSuccess) { this.publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { this.logger.debug("RunAsManager did not change Authentication object"); } return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { this.logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); return new InterceptorStatusToken(origCtx, true, attributes, object); } } else if (this.rejectPublicInvocations) { throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'"); } else { if (debug) { this.logger.debug("Public object - authentication not attempted"); } this.publishEvent(new PublicInvocationEvent(object)); return null; } } }
进入 this.authenticateIfRequired();我们发现Authentication的isAuthenticated状态时false时在这里将会完成再次认证,所以我们自定义认证完Authentication后必须将Authentication的isAuthenticated的状态设置为true,避免后续拦截器再次去认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private Authentication authenticateIfRequired() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication.isAuthenticated() && !this.alwaysReauthenticate) { if (this.logger.isDebugEnabled()) { this.logger.debug("Previously Authenticated: " + authentication); } return authentication; } else { authentication = this.authenticationManager.authenticate(authentication); if (this.logger.isDebugEnabled()) { this.logger.debug("Successfully Authenticated: " + authentication); } SecurityContextHolder.getContext().setAuthentication(authentication); return authentication; } }
认证异常处理 在认证过程中出现认证失败,不管是我们自定义的认证类和Spring security 自带的实现类都抛出AuthenticationException异常,这个异常可以统一由Spring security捕获,从而处理认证异常
认证配置 以下配置中关于认证的我们只需要注意几点
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) 此方法下往ProviderManager中配置了AuthenticationProvider认证器,并且设置了UserDetailService类和设置了认证时密码不加密
public void configure(HttpSecurity http) 此方法配置了认证的具体逻辑,http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class)将我们自定义实现的拦截器加入到UsernamePasswordAuthenticationFilter之前,UsernamePasswordAuthenticationFilter是Spring security中默认最后一个负责认证的拦截器,加入的拦截器负责组装前台提交的账号和密码,将其转换成Authentication。 .exceptionHandling().authenticationEntryPoint负责配置认证异常,所有认证抛出的AuthenticationException及其实现都会在这里配置的AuthenticationEntryPointd 的实现类中处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 /** * @author Liush * @description * @date 2019/11/12 16:06 **/ @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomLoginSuccessHandler customLoginSuccessHandler; @Autowired private CustomLoginFailHandler customLoginFailHandler; @Autowired private CustomLogoutSuccessHandler customLogoutSuccessHandler; @Autowired private JWTUtil jwtUtil; @Autowired private SecurityProperties securityProperties; @Autowired private DbUserDetailsService dbUserDetailsService; @Autowired private PasswordProvider passwordProvider; @Autowired private TokenProvider tokenProvider; @Autowired private CustomEntryPointHandler customEntryPointHandler; @Autowired private IdentityUserServiceI identityUserService; @Autowired private RoleAccessDeniedHandler roleAccessDeniedHandler; @Autowired public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(dbUserDetailsService). passwordEncoder(NoOpPasswordEncoder.getInstance()); authenticationManagerBuilder.authenticationProvider(passwordProvider); authenticationManagerBuilder.authenticationProvider(tokenProvider); } @Bean public RoleAccessSecurityInterceptor roleAccessSecurityInterceptor(){ List<AccessDecisionVoter<? extends Object>> voters=new ArrayList<>(); voters.add(new RoleAccessDecisionVoter()); AccessDecisionManager accessDecisionManager=new RoleDecisionManager(voters); RoleMetadataSource roleMetadataSource=new RoleMetadataSource(jwtUtil,identityUserService); return new RoleAccessSecurityInterceptor(securityProperties.getWhiteUrls(),accessDecisionManager,roleMetadataSource); } @Override public void configure(HttpSecurity http) throws Exception { ValidateFilter validateFilter = new ValidateFilter(securityProperties.getLogin_url(),securityProperties.getWhiteUrls(), jwtUtil, customEntryPointHandler); http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class) //设置认证异常处理器 .exceptionHandling().authenticationEntryPoint(customEntryPointHandler) //设置鉴权异常处理器 .accessDeniedHandler(roleAccessDeniedHandler) .and() .cors() .and() .csrf().disable(); whiteUrlConfig(http); //设置鉴权拦截器 http.addFilterAfter(roleAccessSecurityInterceptor(),FilterSecurityInterceptor.class); } /** * 白名单配置,调用permitAll方法,此url下的连接可以进入security拦截器,但是不鉴权 */ private void whiteUrlConfig(HttpSecurity http) throws Exception { List<String> whiteUrls= WhiteUrlUtil.createWhiteUrls(securityProperties.getWhiteUrls()); for (String url:whiteUrls){ http.authorizeRequests().antMatchers(url).permitAll(); } http.authorizeRequests().anyRequest().authenticated(); } /* @Override public void configure(HttpSecurity http) throws Exception{ http.authorizeRequests().anyRequest().permitAll(); }*/ }
鉴权部分 鉴权就是对用户可以做什么进行判断
FilterInvocation API中给的解释是Holds objects associated with a HTTP filter.意思就是其是保存HTTP过滤器的地方,我们必须在鉴权拦截器中生成FilterInvocation对象,并将FilterInvocation对象传入拦截器链,从而获取http中的信息,因为鉴权很多是和url做关联的,比如什么角色能访问什么url
API中的解释是:提供ConfigAttribute的类实现。
1 2 3 4 5 6 7 public interface SecurityMetadataSource extends AopInfrastructureBean { Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException; Collection<ConfigAttribute> getAllConfigAttributes(); boolean supports(Class<?> var1); }
下面是本人对于这个方法的实现,入参Object var1,是一个FilterInvocation,通过拿到request中的信息,查询到该次请求需要哪些权限(一般是直接获取请求的url,然后去查询这次url访问需要哪些权限),将查询到的需要的权限封装成Collection ,供后续代用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { HttpServletRequest request = ((FilterInvocation) object).getRequest(); String powerId=request.getHeader("powerId"); //查询powerId下需要的角色 List<RoleDTO> roles=identityUserService.findRoleByPowerId(powerId); if(roles==null || roles.isEmpty()){ throw new AccessDeniedException("该权限id不存在"); } List<ConfigAttribute> configAttributes=new ArrayList<>(); for(RoleDTO role:roles){ configAttributes.add(new RoleConfigAttribute(role.getAuthority())); } return configAttributes; }
ConfigAttribute API中的解释:存储与安全系统相关的配置属性
1 2 3 public interface ConfigAttribute extends Serializable { String getAttribute(); }
AccessDecisionManager API中的解释:做出最终访问控制(授权)决定。
1 2 3 4 5 6 7 public interface AccessDecisionManager { void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException; //判断决策管理器时候支持此属性的验证,关于这两个类的实现可参考Spiring security的默认实现 boolean supports(ConfigAttribute var1); boolean supports(Class<?> var1); }
本人实现,获取AbstractAccessDecisionManager中AccessDecisionVoter,进行投票如果有一个AccessDecisionVoter投票为1说明有权限,如果鉴权失败抛出AccessDeniedException异常,由SpringSecurity统一处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class RoleDecisionManager extends AbstractAccessDecisionManager { protected RoleDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) { super(decisionVoters); } @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { int deny = -1; //如果等于-1验证失败,如果等于1验证成功 for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { //通过验证 case AccessDecisionVoter.ACCESS_GRANTED:{ deny=1; break; } //验证失败 case AccessDecisionVoter.ACCESS_DENIED: continue; default: } } if (deny !=1) { throw new AccessDeniedException("没有权限调用此功能"); } // 如果所有投票者都弃权的话 //checkAllowIfAllAbstainDecisions(); } /** * 被AbstractSecurityInterceptor调用,遍历ConfigAttribute集合,筛选出不支持的attribute */ @Override public boolean supports(ConfigAttribute attribute) { return true; } /** * 被AbstractSecurityInterceptor调用,验证AccessDecisionManager是否支持这个安全对象的类型。 */ @Override public boolean supports(Class<?> clazz) { return true; } }
AccessDecisionVoter API中的解释:负责对授权决策进行投票 ,int vote(Authentication var1, S var2, Collection var3)如果有权限返回1,没有权限返回-1
1 2 3 4 5 6 7 8 9 10 11 public interface AccessDecisionVoter<S> { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; boolean supports(ConfigAttribute var1); boolean supports(Class<?> var1); int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); }
AbstractSecurityInterceptor API中的解释:为安全对象实现安全拦截的抽象类
整体认证逻辑 首先我们实现自己的AbstractSecurityInterceptor,在拦截器中生成FilterInvocation对象,调用AbstractSecurityInterceptor的super.beforeInvocation(roleFilterInvocation)完成鉴权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 /** * @author Liush * @description 鉴权拦截器 * @date 2019/11/17 19:39 **/ public class RoleAccessSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private String whiteUrls; private FilterInvocationSecurityMetadataSource securityMetadataSource; public RoleAccessSecurityInterceptor(String whiteUrls, AccessDecisionManager decisionManager,FilterInvocationSecurityMetadataSource securityMetadataSource) { if(StringUtils.isEmpty(whiteUrls)){ whiteUrls= ""; } this.whiteUrls = whiteUrls; super.setAccessDecisionManager(decisionManager); this.securityMetadataSource=securityMetadataSource; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //查看是否白名单,如果是的话不认证 if(WhiteUrlUtil.isWhite((HttpServletRequest) servletRequest,whiteUrls)){ filterChain.doFilter(servletRequest,servletResponse); return; } FilterInvocation roleFilterInvocation=new FilterInvocation(servletRequest,servletResponse,filterChain); InterceptorStatusToken token=super.beforeInvocation(roleFilterInvocation); try{ roleFilterInvocation.getChain().doFilter(servletRequest,servletResponse); }finally { super.afterInvocation(token,null); } } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return securityMetadataSource; } }
我们进入super.beforeInvocation(roleFilterInvocation)方法, Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);此方法调用SecurityMetadataSource来获取,该次调用需要获取什么权限 this.authenticateIfRequired()上面文章提到如果之前如果Authenticate的isAuthenticated为false的话会在这里再次鉴权 this.accessDecisionManager.decide(authenticated, object, attributes);调用AccessDecisionManager进行鉴权投票,如果没有抛出AccessDeniedException异常则鉴权成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); boolean debug = this.logger.isDebugEnabled(); if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass()); } else { Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); if (attributes != null && !attributes.isEmpty()) { if (debug) { this.logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = this.authenticateIfRequired(); try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException var7) { this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7)); throw var7; } if (debug) { this.logger.debug("Authorization successful"); } if (this.publishAuthorizationSuccess) { this.publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { this.logger.debug("RunAsManager did not change Authentication object"); } return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { this.logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); return new InterceptorStatusToken(origCtx, true, attributes, object); } } else if (this.rejectPublicInvocations) { throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'"); } else { if (debug) { this.logger.debug("Public object - authentication not attempted"); } this.publishEvent(new PublicInvocationEvent(object)); return null; } } }
注意 在WebSecurityConfigurerAdapter配置类下配置的白名单 http.authorizeRequests().antMatchers(url).permitAll(),这里配置的url仍然会进入拦截器,所以我在进入拦截器前先会去查找白名单,如果是白名单直接跳过拦截器