SpringSecurity原理解析

Posted weixin_42412601

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity原理解析相关的知识,希望对你有一定的参考价值。

目录

1、认证流程

总体的认证流程如下:

登录的请求首先会被拦截器UsernamePasswordAuthenticationFilter拦截,第一次请求肯定是未认证,未认证通过对象AuthenticationManager,来委托AuthenticationProvider,去关联UserDetailService,去查数据库,判断用户是否是数据库中存在的用户,当认证通过后,把数据封装到UserDetail中,再然后把认证 的信息,封装到Authentication

源码查看

UsernamePasswordAuthenticationFilter认证过滤器,继承自AbstractAuthenticationProcessingFilter
它的doFilter方法,其实是在父类AbstractAuthenticationProcessingFilter中,它只是重写了一些方法而已。

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        1、判断当前请求是不是post请求
        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 
            	//2、调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面去
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) 
                    return;
                
				//3、认证成功之后,用session存储相关信息。session策略处理
                this.sessionStrategy.onAuthentication(authResult, request, response);
             catch (InternalAuthenticationServiceException var8) 
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                //4、认证失败,做认证失败的处理
                this.unsuccessfulAuthentication(request, response, var8);
                return;
             catch (AuthenticationException var9) 
            	//4、认证失败,做认证失败的处理
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            
			//5、认证成功后的处理
            if (this.continueChainBeforeSuccessfulAuthentication) 
            	//执行下一个过滤器
                chain.doFilter(request, response);
            
			//调用认证成功后的方法
            this.successfulAuthentication(request, response, chain, authResult);
        
    

1、判断当前请求是不是post请求

这个requiresAuthenticationRequestMatcher其实最终是来源于子类UsernamePasswordAuthenticationFilter的构造方法
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) 
     return this.requiresAuthenticationRequestMatcher.matches(request);
 
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) 
     Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
     this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;

UsernamePasswordAuthenticationFilter的构造方法
public UsernamePasswordAuthenticationFilter() 
     super(new AntPathRequestMatcher("/login", "POST"));

如果我们自定义一个认证过滤器,只需要继承自UsernamePasswordAuthenticationFilter,然后在构造方法中,调用setRequiresAuthenticationRequestMatcher就能覆盖UsernamePasswordAuthenticationFilter设置的默认的登录路径。

2、调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面去

UsernamePasswordAuthenticationFilter

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 		
    	//判断是否是post请求
        if (this.postOnly && !request.getMethod().equals("POST")) 
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         else 
        	//从请求的queryString中获取用户名和密码
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) 
                username = "";
            

            if (password == null) 
                password = "";
            

            username = username.trim();
            //将用户名和密码封装到UsernamePasswordAuthenticationToken中
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            //再将request请求,封装到UsernamePasswordAuthenticationToken对象中
            this.setDetails(request, authRequest);
            //将未认证的信息调用authenticate方法进行认证
            return this.getAuthenticationManager().authenticate(authRequest);
        
    
//从请求的queryString中获取用户名和密码   
protected String obtainUsername(HttpServletRequest request) 
   return request.getParameter(this.usernameParameter);

2.1、查看UsernamePasswordAuthenticationToken


该类是Authentication的实现类

2.2、调用authenticate方法进行身份认证

ProviderManagerAuthenticationManager 接口的实现类,该接口是认证相关的核心接口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个List<AuthenticationProvider>列表,存放多种认证方式,实际上这是委托者模式(Delegate)的应用。每种认证方式对应着一个 AuthenticationProviderAuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托对应的 AuthenticationProvider 进行用户认证。

ProviderManagerauthenticate方法的认证过程。

    public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    	//获取传入的Authentication 类型的UsernamePasswordAuthenticationToken.class
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        //获取认证方式列表List<AuthenticationProvider>的迭代器
        Iterator var8 = this.getProviders().iterator();
		//循环迭代
        while(var8.hasNext()) 
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            //判断当前的AuthenticationProvider是否支持UsernamePasswordAuthenticationToken.class类型的Authentication 
            if (provider.supports(toTest)) 
                if (debug) 
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                
                try 
                	//重点!!!!!!!!!!!!!!!!!!!!!!!!!!!
                	//成功找到适配当前认证方式的AuthenticationProvider,此处为DaoAuthenticationProvider
                	//如果认证成功,会返回一个标记已认证的Authentication对象
                    result = provider.authenticate(authentication);
                    if (result != null) 
                    	//认证成功,将传入的Authentication对象中的details信息拷贝到已认证的Authentication对象中
                        this.copyDetails(authentication, result);
                        break;
                    
                 catch (InternalAuthenticationServiceException | AccountStatusException var13) 
                    this.prepareException(var13, authentication);
                    throw var13;
                 catch (AuthenticationException var14) 
                    lastException = var14;
                
            
        
        if (result == null && this.parent != null) 
            try 
            	//认证失败,使用父类型的AuthenticationManager进行验证
                result = parentResult = this.parent.authenticate(authentication);
             catch (ProviderNotFoundException var11) 
             catch (AuthenticationException var12) 
                parentException = var12;
                lastException = var12;
            
        
        if (result != null) 
        	//认证成功之后,去除result的敏感信息,要求相关类实现CredentialsContainer接口
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) 
            	//去除过程就是调用CredentialsContainer接口的eraseCredentials方法
                ((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;
        
    

看重点:DaoAuthenticationProviderauthenticate方法,实际上是父类AbstractUserDetailsAuthenticationProviderauthenticate方法

    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;
        //尝试从缓存中获取user
        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);
    

看重点:发现retrieveUser方法被子类DaoAuthenticationProvider,重写了

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException 
    this.prepareTimingAttackProtection();

    try 
    	//获取一个UserDetailsService,并执行它的loadUserByUsername方法,来获取一个用户
    	//查询数据库获取用户的逻辑,就写在loadUserByUsername中
        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);
    

认证过程最重要的部分就解析完了

3、认证成功/失败处理

回到AbstractAuthenticationProcessingFilter,查看successfulAuthenticationunsuccessfulAuthentication方法

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);
    
	//将认证成功的用户信息对象Authentication封装进SecurityContext对象中
	//SecurityContextHolder是对ThreadLocal的一个封装
    SecurityContextHolder.getContext().setAuthentication(authResult);
    //remenberMe的处理
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) 
    	//发布认证成功的事件
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    
	//调用认证成功处理器
    this.successHandler.onAuthenticationSuccess(request, response, authResult);

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException 
	//清除该线程在SecurityContextHolder中对应的SecurityContext对象
     SecurityContextHolder.clearContext();
     if (this.logger.isDebugEnabled()) 
         this.logger.debug("Authentication request failed: " + failed.toString(), failed);
         this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
         this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
     
	 //rememberMe的处理
     this.rememberMeServices.loginFail(request, response);
     //调用认证失败处理器
     this.failureHandler.onAuthenticationFailure(request, response, failed);
 

2、授权流程

上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。

2.1、 ExceptionTranslationFilter 过滤器

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:

2.2、 FilterSecurityInterceptor过滤器

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器。

ExceptionTranslationFilter 进行捕获和处理。具体源码如下:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
     FilterInvocation fi = new FilterInvocation(request, response, chain);
     this.invoke(fi);
 
public void invoke(FilterInvocation fi) throws IOException, ServletException 
	if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) 
	    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	 else 
	    if (fi.getRequest() != null && this.observeOncePerRequest) 
	        fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
	    
		//根据资源权限配置来判断当前请求是否有权限访问对应的资源
		//如果不能访问则抛出异常
	    InterceptorStatusToken token = super.beforeInvocation(fi);
	
	    try 
	    	//访问相关资源,通过Springmvc的核心组件DispatchServlet进行访问
	        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	     finally 
	    	//待请求完成后会在 finallyInvocation() 中将原来的 SecurityContext 重新设置给SecurityContextHolder。
	        super.finallyInvocation(token);
	    
         // 正常请求结束,最后也会执行(afterInvocation 内部会调用finallyInvocation )
	    super.afterInvocation(token, (Object)null);
	

需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

看看beforeInvocation

    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 
                	//使用获取到的ConfigAttribute ,继续调用访问控制器AccessDecisionManager对当前请求进行鉴权。
                	//无论鉴权通过或是不通后,Spring Security 框架均使用了观察者模式,来通知其它Bean,当前请求的鉴权结果。
                	//如果鉴权不通过,则会抛出 AccessDeniedException 异常,即访问受限,然后会被 ExceptionTranslationFilter 捕获,最终解析后调转到对应的鉴权失败页面
                	//如果鉴权通过,AbstractSecurityInterceptor 通常会继续请求
                    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));
                
				//通过 RunAsManager 在现有 Authentication 基础上构建一个新的Authentication
                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());
                    //如果新的 Authentication 不为空则将产生一个新的 SecurityContext,并把新产生的Authentication 存放在其中
                    //这样在请求受保护资源时从 SecurityContext中 获取到的 Authentication 就是新产生的 Authentication。
                    SecurityContextHolder.getContext().setAuthentication(runAs);
                    return new InterceptorStatusToken(origCtx, true, attributes, object);
                
             // rejectPublicInvocations 属性,默认为 false。此属性含义为拒绝公共请求   
             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;
            
        
    

authenticateIfRequired:判断是否需要进行身份认证

private Authentication authenticateIfRequired() 
	//从SecurityContextHolder获取authentication 
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    //判断是否已经认证过了。
    //还记得UsernamePasswordAuthenticationToken的构造方法有设置是否认证过吗
    if (authentication.isAuthenticated() && !this.alwaysReauthenticate) 
        if (this.logger.isDebugEnabled()) 
            this.logger.debug("Previously Authenticated: " + authentication);
        
		//认证过了,直接返回authentication
        return authentication;
     else 
    	//没有认证过,调用相应的ProviderManager去认证
        authentication = this.authenticationManager.authenticate(authentication);
        if (this.logger.isDebugEnabled

以上是关于SpringSecurity原理解析的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 解析 —— 基于JWT的单点登陆(SSO)开发及原理解析

coding++:Spring 中的 AOP 原理

SpringSecurity登录原理(源码级讲解)

将99个鸡蛋放入8个篮子中,不论怎么放,总有一个篮子里放入13个鸡蛋,为啥?

轻松上手SpringSecurity,OAuth,JWT

轻松上手SpringSecurity,OAuth,JWT