ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Spring-Security集成Oauth2.0(上)

2022-03-31 00:32:40  阅读:219  来源: 互联网

标签:null Spring object Authentication debug fi Security Oauth2.0 public


集成之前首先介绍一下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,其它的异常它会继续抛出

  • 介绍完总体结构我们来看一下认证流程的流程:
      •  先看下面的流程图

  1.  
      • 首先用户表单提交的用户名密码经过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中的
        Authentication authenticate(Authentication var1) throws AuthenticationException;
        方法进行认证,他的实现类是ProviderManager,由实现类进行认证根据源码可以看到ProviderManagerList<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的,因为我们是表单登录,它的实现类是DaoAuthenticationProvider,其中维护着一个UserDetialService类用于对用户的账号密码进行获取并检验,可以看以下源码(此类要重写UserDetailService中的loaduserByUsername()方法,用户获取本地的用户信息)
      • 查看代码
        
        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有三个实现类,分别表示不同的规则的授权,分别是AffiffiffirmativeBasedConsensusBasedUnanimousBased,当然用户也可以自己实现。
        • 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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有