Spring-Security集成Oauth2.0(上)
作者:互联网
集成之前首先介绍一下Security的实现原理;
-
初始化SpringSecurity;
- 会创建一个SpringSecurityFilterChin的Servlet的过滤器,它是一个链式的j结构如下;
- FilterChainProxy是一个代理,真真起作用的时Filter中的SecurityFilterChain所包含的Filter,这些Filter作为Bean被Spring管理,这些才是SpringSecurity的核心,但是他们不直接处理认证,也不直接处理用户授权,而是把他们交给认证管理器AuthenticationManager和决策管理器AccessDecisionManager进行处理;
SecurityContextPersistenceFilter:拦截器的出口和入口,会在开始的时候从配置好的SecurityContextRepository中获取SecurityContext,然后设置给SecurityContextHolder。请求完成后将SecurityContextHolder中持有的SecurityContext保存到SecurityContextRepository,再清除SecurityContextHolder中持有的SecurityContext,我们可以看看Security中SecurityContext的源代码,其中包含的Authentication(PS:在分布式中网关转发的时候需要将Authentication中的信息报存到Request中,不然后丢失,Authentication中包含了用户的详细信息,后文会详细讲到)。
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = 510L;
private Authentication authentication;
public SecurityContextImpl() {
}
UsernamePasswordAuthenticationFilter:处理用户提交的来自表单等方式的认证,这里我们演示的是表单认证方式(它还可以用户名密码提交,短信验证登录等)
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
}
FilterSecurityInterceptor:保护web资源,使用AccessDecisionManager对当前用户进行授权访问(AccessDecisionManager是起决策作用,security自带几种决策管理器,用于判断是否通过认证授权)
ExceptionTranslationFilter:捕获FilterChain的异常,并进行处理,不过它通常处理两类异常,AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出
- 介绍完总体结构我们来看一下认证流程的流程:
-
-
- 先看下面的流程图
-
-
-
- 首先用户表单提交的用户名密码经过UsernamePasswordAuthenticationFilter过滤器得到,放入Authentication对象中(此对象专门用于存储用户提交的上文所说的,账号,密码等用户信息)
- 查看代码
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
- 根据源码可以看到,最后将信息放在UsernamePasswordAuthenticationToken(它实际上实现了Authentication接口)
- 然后进入AuthenticationManager中的
方法进行认证,他的实现类是ProviderManager,由实现类进行认证根据源码可以看到ProviderManagerList<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的,因为我们是表单登录,它的实现类是DaoAuthenticationProvider,其中维护着一个UserDetialService类用于对用户的账号密码进行获取并检验,可以看以下源码(此类要重写UserDetailService中的loaduserByUsername()方法,用户获取本地的用户信息)Authentication authenticate(Authentication var1) throws AuthenticationException;
- 查看代码
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 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); } }
- 最后通过认证方式获取UserDetail信息,和从UsernamePasswordAuthenticationFilter过滤器得到的Authentication对象通过ProviderManager中authenticate(Authentication authentication) 方法进行比对认证
-
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; } } }
成功着返回认证通过后的Authentication ,其中删除密码等信息,失败着抛出异常。
- 以下是笔者自己实现的UserDetailService类
查看代码
public class SpringDataUserDetailsService implements UserDetailsService { @Autowired UserDao userDao; //根据 账号查询用户信息 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //将来连接数据库根据账号查询用户信息 UserDto userDto = userDao.getUserByUsername(username); if(userDto == null){ //如果用户查不到,返回null,由provider来抛出异常 return null; } //根据用户的id查询用户的权限 List<String> permissions = userDao.findPermissionsByUserId(userDto.getId()); //将permissions转成数组 String[] permissionArray = new String[permissions.size()]; permissions.toArray(permissionArray); //将userDto转成json String principal = JSON.toJSONString(userDto); UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build(); return userDetails; } }
- 可以看到最后UserDetailService里面还天界了一个authorities(permissionArray),这就是认证通过后,这个用户所拥有的授权信息,也就是我们接下来将会讲到的第二部分Security授权
-
- 接下来是授权流程
-
- 先看一下授权的流程图
-
-
-
- 可以看出,授权是FilterSecurityInterceptor拦截器拦截的,在这个拦截器中会从A1处调用SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource获取当前需要访问的权限Collection<ConfigAttribute>
-
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) { this.securityMetadataSource = newSource; } public Class<?> getSecureObjectClass() { return FilterInvocation.class; } public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //A1 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } } }
- 进入A1标识处的方法会发现,调用父类的beforeInvocation()方法,父类接受的Object实际上就是用户访问的url:/*/**,其中下面B2处获取的就是该地址所需要的Attributes(该地址访问的权限,需要web配置,后续会有笔者配置实列)
查看代码
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!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: " + getSecureObjectClass()); } //B2 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (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'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //B3 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
- 配置的Attributes
-
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //认证管理器 @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } //密码编码器 @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } //安全拦截机制(最重要) @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/r/r1").hasAnyAuthority("p1") .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin() ; }
- B2调用的是FilterSecurityInterceptor中SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource的getAttributes()方法;
- 授权的最后,是调用B3处AccessDecisionManager对象进行决策授权
-
public interface AccessDecisionManager { void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; }
authentication:是登录用户所拥有的权限信息,object访问的url地址,configAttributes用户配置的访问该地址所需要的权限。
-
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。
public interface AccessDecisionVoter<S> { int ACCESS_GRANTED = 1; //表示同意 int ACCESS_ABSTAIN = 0; //表示拒绝 int ACCESS_DENIED = -1;//表示弃权 boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); }
如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。
- 值得一提的是AccessDecisionManager有三个实现类,分别表示不同的规则的授权,分别是AffiffiffirmativeBased、ConsensusBased和UnanimousBased,当然用户也可以自己实现。
- AffiffiffirmativeBased:只要有一个AccessDecisionVoter同意就通过
-
-
-
-
- ConsensusBased:反对多余同意就不通过
-
-
-
-
-
- UnanimousBased:全部同意才能通过
-
-
- 最后通过投票就表示认证授权流程完成了,下一篇将会讲Security集成Oauth2.0;
参考:https://www.liangzl.com/get-article-detail-4650.html
标签:null,Spring,object,Authentication,debug,fi,Security,Oauth2.0,public 来源: https://www.cnblogs.com/freedomBird/p/16069739.html