SpringSecurity總結

首先提供一個總體的配置文件,再分析:css

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:s="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.1.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.1.xsd">
        
    <!-- 不要過濾圖片等靜態資源,其中**表明能夠跨越目錄,*不能夠跨越目錄。 -->
	<s:http pattern="/**/*.jpg" security="none"/>
	<s:http pattern="/**/*.png" security="none"/>
	<s:http pattern="/**/*.gif" security="none"/>
	<s:http pattern="/**/*.css" security="none"/>
	<s:http pattern="/**/*.js" security="none"/>
	<s:http pattern="/login.jsp" security="none"/>
	<s:http pattern="/user/user!login.action" security="none"/>
	
	<s:http auto-config="false" use-expressions="true" entry-point-ref="loginUrlEntryPoint" access-denied-page="/error.jsp">
		<!-- 處理用戶登錄時,用戶名和密碼判斷的filter(重要) -->
		<s:custom-filter position="FORM_LOGIN_FILTER" ref="loginFilter"/>
		<!-- 權限判斷、處理的filter鏈 -->
		<s:custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="myFilter"/>
		<!-- 檢測失效的sessionId,超時時定位到另一個URL -->
		<s:session-management invalid-session-url="/sessionTimeout.jsp" />
		<!-- 登出 -->
		<s:logout logout-url="/j_spring_security_logout" invalidate-session="true" logout-success-url="/index.jsp" />
	</s:http>
    
	<!-- 一個自定義的filter,必須包含authenticationManager, accessDecisionManager,securityMetadataSource三個屬性。 -->
	<bean id="myFilter" class="com.dtds.security.MyFilterSecurityInterceptor">
		<!-- 資源數據源 -->
		<property name="securityMetadataSource" ref="mySecurityMetadataSource" />
		<!-- 認證管理器 -->
		<property name="authenticationManager" ref="authenticationManager" />
		<!-- 訪問決策器 -->
		<property name="accessDecisionManager" ref="accessDecisionManager" />
	</bean>
		
	<!-- 資源源數據定義,將全部的資源和權限對應關係創建起來,即定義某一資源能夠被哪些角色去訪問。 -->
	<bean id="mySecurityMetadataSource" class="com.dtds.security.InvocationSecurityMetadataSourceServiceImpl">
		<property name="userService" ref="userService"/>
	</bean>

	<!-- 認證配置, 使用userDetailsService提供的用戶信息 -->
	<s:authentication-manager alias="authenticationManager">
		<s:authentication-provider ref="authenticationProvider"/>
	</s:authentication-manager>
	
	<!-- 能夠重寫 -->
	<!-- 
	<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">  
	 -->
	<bean id="authenticationProvider" class="com.dtds.security.SecurityAuthenticationProvider">  
	    <property name="userDetailsService" ref="userDetailsService" />
	    <property name="hideUserNotFoundExceptions" value="false" />
	    <property name="passwordEncoder" ref="passwordEncoder"/>
	</bean>
    <!-- 登錄時查詢用戶、並加載用戶所擁有的權限等 -->
	<bean id="userDetailsService" class="com.dtds.security.UserDetailServiceImpl">
		<property name="userService" ref="userService"/>
	</bean>
	<!-- 用戶的密碼加密或解密 -->
	<bean id="passwordEncoder" class="com.dtds.security.MyPasswordEncoder" />
	
	<!-- 訪問決策器,決定某個用戶具備的角色,是否有足夠的權限去訪問某個資源。 -->
	<bean id="accessDecisionManager" class="com.dtds.security.MyAccessDecisionManager"/>

	<!-- 重寫登錄驗證 -->
	<bean id="loginFilter" class="com.dtds.security.MyUsernamePasswordAuthenticationFilter">
		<!-- 認證管理 -->
		<property name="authenticationManager" ref="authenticationManager"></property>
		<!-- 驗證成功後的跳轉 -->
		<property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"></property>
		<!-- 處理登錄的Action -->
		<property name="filterProcessesUrl" value="/j_spring_security_check"></property>
		<!-- 驗證失敗後的處理 -->
		<property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandler"></property>
	</bean>
	<!-- 登錄成功 -->
	<!-- 這裏要實現自定義 -->
	<!-- 
	<bean id="loginLogAuthenticationSuccessHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
	 -->
	<bean id="loginLogAuthenticationSuccessHandler" class="com.dtds.security.LoginAuthenticationSuccessHandler">
		<property name="alwaysUseDefaultTargetUrl" value="true"/>
		<!-- 登錄成功時的頁面,這裏設定的頁面也會被spring security攔截 -->
		<!-- 
		<property name="defaultTargetUrl" value="/content/select.jsp" />
		<property name="targetUrlParameter" value="redirectTo" />
		 -->
		<property name="defaultTargetUrl" value="/user/user!login.action" />
	</bean>
	<!-- 登錄失敗 -->
	<bean id="simpleUrlAuthenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<!-- 能夠配置相應的跳轉方式。屬性forwardToDestination爲true採用forward false爲sendRedirect -->
		<property name="defaultFailureUrl" value="/login.jsp"></property>
	</bean>
	<!-- 未登陸的切入點 -->
	<bean id="authenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/login.jsp"></property>
	</bean>
	
	<!-- Spring Security 認證切入點 -->
	<bean id="loginUrlEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/login.jsp"></property>
	</bean>
	
</beans>

 

首先看到,用戶登錄的時候嗎請求會被這個配置攔截:java

<!-- 處理用戶登錄時,用戶名和密碼判斷的filter(重要) -->
		<s:custom-filter position="FORM_LOGIN_FILTER" ref="loginFilter"/>

能夠看到這個loginFilter是web

<!-- 重寫登錄驗證 -->
	<bean id="loginFilter" class="com.dtds.security.MyUsernamePasswordAuthenticationFilter">

它包含了四個屬性,首先看第一個spring

authenticationManager數據庫

它最終指向的是authenticationProvider,即一個權限判斷的提供者,它又包含了三個屬性,主要關注的是userDetailsServicepasswordEncoderexpress

即用戶名和密碼的校驗,後面跟進代碼會再次回到這兩個屬性上~!(其實用戶名和密碼都正確的話,理論上就是經過了登錄權限的校驗)緩存

 

看看這個過濾器,它主要的功能是由方法attemptAuthentication提供,最終的目的返回一個權限對象Authentication,在該方法中:session

主要是這句:app

Authentication authentication = super.attemptAuthentication(request, response);jsp

因爲

MyUsernamePasswordAuthenticationFilter

是繼承自UsernamePasswordAuthenticationFilter過濾器,那麼這裏使用了super,即實際執行的是父類的方法,源碼以下:

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

該方法的入參就是request和response,接着執行,就獲取到頁面輸入的用戶名和密碼,因爲這裏返回的是一個權限對象,能夠看到最終返回的便是這句:

this.getAuthenticationManager().authenticate(authRequest);

在返回前,並未看到SS對用戶名和密碼作任何驗證,那麼驗證確定在上述這句代碼中,跟進去,進入到ProviderManager的authenticate方法,源碼以下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            } catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to invalid account status
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            } catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already handled the request
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data from authentication
                ((CredentialsContainer)result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",
                        new Object[] {toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }

 

執行到這句:

result = provider.authenticate(authentication);

繼續跟進,進入:AbstractUserDetailsAuthenticationProvider的authenticate方法,源碼:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            } catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                } else {
                    throw notFound;
                }
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        } catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
            } else {
                throw exception;
            }
        }

        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

 

這裏看出ss首先去緩存中獲取user對象,若是沒有獲取到,則繼續執行,到這句

                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

跟進,則進入:

DaoAuthenticationProvider的retrieveUser方法,源碼:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException notFound) {
            if(authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null);
            }
            throw notFound;
        } catch (Exception repositoryProblem) {
            throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new AuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }

這裏關鍵代碼:

loadedUser = this.getUserDetailsService().loadUserByUsername(username);

即便用UserDetailService的loadUserByUsername方法,而UserDetailService是一個接口,該接口正是須要咱們本身實現的關鍵點,它只有惟一的一個方法loadUserByUsername,該方法須要咱們本身實現,主要就是經過用戶名從數據庫中獲取用戶並判斷

跟進這麼多,最終獲得如同上面說的結果:回到userDetailsServicepasswordEncoder

首先是userDetailsService

它主要是獲取用戶及權限,該方法返回的是一個Spring定義的User,按要求封裝便可,能夠本身建立一個MyUser類,繼承該User或者直接返回便可

返回以後,繼續在DaoAuthenticationProvider中執行,執行完畢以後繼續在AbstractUserDetailsAuthenticationProvider中執行

執行到這句:

preAuthenticationChecks.check(user);

源碼:

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"), user);
            }

            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"), user);
            }

            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"), user);
            }
        }
}

這是一個內部類,能夠看到是判斷user的一些其餘屬性

接着執行下面一句:

additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

能夠看到這個方法最終會執行咱們本身繼承的類

SecurityAuthenticationProvider

方法:

@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		logger.info("【3】:SecurityAuthenticationProvider");
		
		//執行到這句話,下面就會執行MyPasswordEncoder
		super.additionalAuthenticationChecks(userDetails, authentication);

		WebAuthenticationDetails webDetail = (WebAuthenticationDetails) authentication.getDetails();

		try {
			this.logger.warn("開始寫currentLogin表");
			long start = System.currentTimeMillis();
			//this.POService.additionalLoginCheck(peopleInfo);
			long end = System.currentTimeMillis();
			this.logger.warn("寫currentLogin表完畢,用時:" + (end - start) + "ms");
		} catch (Exception e) {
			this.logger.error("寫currentLogin表異常", e);
			throw new AuthenticationServiceException(e.getMessage());
		}
	}

 

這裏能夠添加一些其餘的應用

該方法使用super又調用了父類的方法

DaoAuthenticationProvider

方法

@SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails);
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails);
        }
    }

 

能夠看到繼續是一些驗證代碼

其中這句:

String presentedPassword = authentication.getCredentials().toString();

獲取的便是用戶輸入的密碼,下面調用密碼判斷的代碼:

if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails);
        }

這裏能夠看到倒數第二個MyPassWordEncoder,便是咱們本身實現的密碼驗證類,以下:

@Override
	public boolean isPasswordValid(String npwd, String opwd, Object arg2)
	{
		System.out.println("3:MyPasswordEncoder.isPasswordValid().......validating password..........");
		System.out.println("輸入的密碼:" + opwd + ",存儲的密碼" + npwd);
		if(npwd.equals(opwd)){
			System.out.println("密碼正確");
		} else {
			System.out.println("密碼錯誤");
		}
		return npwd.equals(opwd);
	}

這裏只是簡單的驗證而已,能夠添加其餘的

自此,DaoAuthenticationProvider已經執行完畢,代碼繼續回到:SecurityAuthenticationProvider中,執行完畢,繼續執行AbstractUserDetailsAuthenticationProvider,知道執行最後一句返回代碼:

return createSuccessAuthentication(principalToReturn, authentication, user);

它是本類裏面的方法,跟進:

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
}

從名字能夠看出,如今已是符合權限的要求了create-success-Authentication

看這句

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,

                authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities()));

這裏的user就是咱們自定義的繼承自spring User的MyUser,因爲重寫了

public Collection<GrantedAuthority> getAuthorities() {
		List list = new ArrayList();
		for (String sid : this.roleInfos) {
			list.add(new GrantedAuthorityImpl(sid));
		}
		return list;
	}

因此此時執行的是咱們本身的getAuthrities(),注意這裏返回的result

其中的principal存儲的是User,因此其餘方法調用getPrincipal()獲得的就是User

自此AbstractUserDetailsAuthenticationProvider執行完畢

繼續執行ProviderManager#authenticate()

因爲result不爲null,則執行:

if (result != null) {

                    copyDetails(authentication, result);

                    break;

                }

 

繼續執行,則ProviderManager執行完畢,接着執行UsernamePasswordAuthenticationFilter,返回authentication,執行完畢。

再執行MyUsernamePasswordAuthenticationFilter

獲得了authentication對象並返回,MyUsernamePasswordAuthenticationFilter執行完畢。

自此這個過濾器AbstractAuthenticationProcessingFilter執行到doFilter,獲得正確的權限對象,最後執行:successfulAuthentication(request, response, chain, authResult);

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException{
        successfulAuthentication(request, response, authResult);
}

繼續執行

@Deprecated
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            Authentication authResult) throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
}

這裏能夠看出,將權限信息set到了SecurityContextHolder中,接着執行rememberMeService。。

最後一句:

successHandler.onAuthenticationSuccess(request, response, authResult);

因爲上述的權限認證已經經過,因此這裏就調用successHandler,即登陸成功後的處理類,這個類已經被咱們實現了,因此就執行LoginAuthenticationSuccessHandler的onAuthenticationSuccess方法,在這裏面就是執行咱們本身的流程,其中最重要的就是將用戶對象set到session中,供整個web使用

後面就返回了,繼續執行過濾器鏈

相關文章
相關標籤/搜索