深入理解SpringSecurity的执行原理

SpringSecurity最主要的就是过滤器链



一、过滤器链的原理分析

首先分析web.xml中的如下配置:

	 <filter>
	    <filter-name>springSecurityFilterChain</filter-name>
	    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	  </filter>
	  <filter-mapping>
	    <filter-name>springSecurityFilterChain</filter-name>
	    <url-pattern>/*</url-pattern>
	  </filter-mapping>

找到DelegatingFilterProxy这个类,其实这个类还是过滤器。

Alt
Alt

查看doFilter方法。

Alt

debug查看上面的方法。所以web.xml中Filter的名字必须是springSecurityFilterChain也就不奇怪了。

Alt


继续查看上述方法返回的FilterChainProxy这个类。

Alt


还是过滤器,继续看doFilter方法。

Alt
继续debug上述方法。

Alt

继续进入getFilters方法。

Alt

SecurityFilterChain其实是个接口,实现关系如下图:

Alt



二、CSRF过滤器分析

       CSRF跨站点请求伪造(Cross—Site Request Forgery),存在巨大的危害性。

   CSRF攻击攻击原理及过程如下:

   1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

   2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

   3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;

   4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

   5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。 

       现在想明白了,为什么我之前的QQ号被盗了。。。裂开啊。。。那肯定是小杂碎通过诱惑的手段,让我去点击某个网址,并在该网址上造了一个URL(群发骚扰消息的请求,也是之前的诱惑网址,发给几个群之后,修改自己密码为杂碎自己定义的密码),OK接下来我密码直接没了。联想CSRF攻击,我在点那个网址的时候,相当于我把手机上的缓存信息也带过去了,就相当于黑客伪造我在发请求,emmm,信息安全很重要呐。


接下来看一下SpringSecurity提供的CsrfFilter过滤器,
Alt

点到最后会发现,如下图。

Alt

非GET等如下请求都会经过Csrf过滤器,而GET请求等直接放行。要想通过CSRF过滤器,可以手动添加csrf的token信息,可通过SpringSecurity提供的动态标签直接添加。

Alt




三、认证的过滤器分析

Alt


UsernamePasswordAuthenticationToken类中其实就包括了用户名和密码两部分。

Alt


继续进入认证方法。

Alt

SpringSecurity自带的用户对象为UserDetails 。

	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就是SpringSecurity自己的用户对象
        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方法查看认证业务。

Alt

	public interface UserDetailsService {
	    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
	}
	public interface UserDetails extends Serializable {
	    Collection<? extends GrantedAuthority> getAuthorities();
	
	    String getPassword();
	
	    String getUsername();
	
	    boolean isAccountNonExpired();
	
	    boolean isAccountNonLocked();
	
	    boolean isCredentialsNonExpired();
	
	    boolean isEnabled();
	}

所以我们自己通过数据库实现认证是需要实现UserDetailsService 接口,并返回SpringSecurity提供的用户对象UserDetails即可。

再看之前源码中的最后一句createSuccessAuthentication方法。

 	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

这里又封装了UsernamePasswordAuthenticationToken 对象,但是和之前不同的是,此时多了角色信息

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

顺便看一下authorities参数的来源,来到实现了GrantedAuthoritiesMapper接口到的父类中。

Alt
然而一顿操作猛如虎,一看战绩0-5的感觉油然而生。

Alt

接着看super(authorities)方法。

Alt

找到最开始的UsernamePasswordAuthenticationFilter的父类,并查看父类实现的doFilter方法。

	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);
        }
    }

Alt




四、查看记住我源码分析

找到接口的实现类。

Alt

Alt

判断表单中记住我标记的具体逻辑。

	protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        if (this.alwaysRemember) {
            return true;
        } else {
            String paramValue = request.getParameter(parameter);
			
			//忽略大小写的on、true、yes以及1都可以通过
            if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
                return true;
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
                }

                return false;
            }
        }
    }

继续查看父类PersistentTokenBasedRememberMeServices实现的onLoginSuccess方法。
Alt

SpringSecurity需要手动开启其开启remember me的过滤器,并且持久化到数据库表时表名以及各个字段名都是固定的,无法改变,表结构如下:

	CREATE TABLE persistent_logins(
	  username varchar(64) NOT NULL,  
		series varchar(64) NOT NULL,  
		token varchar(64) NOT NULL,  
		last_used timestamp NOT NULL,  
		PRIMARY KEY(series) 
	)ENGINE=InnoDB DEFAULT CHARSET=utf8;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章