深入理解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;

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