1. 程式人生 > >spring-security認證過程的分析及自定義登入

spring-security認證過程的分析及自定義登入

首先spring-security配置認證過濾器,它是spring-security處理業務的入口。使用者如果不重寫過濾器,使用預設的過濾器UsernamePasswordAuthenticationFilter。它繼承了抽象類AbstractAuthenticationProcessingFilter,該類注入了authenticationManager屬性,配置security的認證管理器。

<beans:bean id="myLoginFilter" class="com.yinhai.modules.security.spring.app.filter.Ta3AuthenticationFilter"
>
<!--認證管理器--> <beans:property name="authenticationManager" ref="myAuthenticationManager" /> <!-- 驗證成功後執行擴充套件的處理 --> <beans:property name="authenticationSuccessHandler" ref="taOnAuthenticationSuccessHandler" /> <!-- 驗證失敗後執行擴充套件的處理 --> <beans:property
name="authenticationFailureHandler" ref="taAuthenticationFailureHandler" />
<beans:property name="filterProcessesUrl" value="/j_spring_security_check" /> <beans:property name="userBpo" ref="userBpo" /> <beans:property name="sessionAuthenticationStrategy" ref="sas"></beans:property
>
</beans:bean>

過濾器UsernamePasswordAuthenticationFilter拿到使用者名稱密碼建立一個UsernamePasswordAuthenticationToken,通過認證管理器進行認證程式碼如下

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

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

        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

認證管理器配置如下

<authentication-manager alias="myAuthenticationManager">  
    <authentication-provider user-service-ref="taUserDetailsService">  
        <password-encoder ref="md5Encoder">
           <salt-source ref="saltSource"/>
            </password-encoder>  
    </authentication-provider>  
</authentication-manager>

認證管理器是通過介面AuthenticationManager處理的

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

ProviderManager類實現了這個介面,它的認證程式碼如下

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

        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.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 var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

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

            this.prepareException((AuthenticationException)lastException, authentication);
            throw lastException;
        }
    }
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
result = provider.authenticate(authentication);

看以上程式碼,認證過程繼續呼叫AuthenticationProvider,它是一個介面,呼叫抽象類AbstractUserDetailsAuthenticationProvider進行認證。

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

DaoAuthenticationProvider類繼承了抽象類AbstractUserDetailsAuthenticationProvider,重寫了獲取使用者的方法,通過配置中的user-service-ref,呼叫獲取UserDetails的方法。

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

接下來對使用者進行驗證如果沒有配置password-encoder,預設呼叫PlaintextPasswordEncoder進行密碼的對比。否則呼叫使用者配置的密碼加密類進行密碼比對。

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) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

基於上述分析,現我們有如下業務需求,通過qq等第三方登入,繫結系統的賬號,通過繫結賬號直接進行security的登入,那麼我們該怎麼做呢?主要程式碼如下

String userName = request.getParameter("userName");
String pwd = request.getParameter("pwd");
userName = userName.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, pwd);
Authentication authentication = authenticationManager.authenticate(authRequest); 
            //呼叫loadUserByUsername  SecurityContextHolder.getContext().setAuthentication(authentication);
HttpSession session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());// 這個非常重要,否則驗證後將無法登陸

通過繫結使用者查詢資料庫,獲取使用者密碼,手動建立一個UsernamePasswordAuthenticationToken,呼叫配置的認證管理器,返回一個認證令牌,註冊到security容器中。即可。

這裡有一個問題,如果user-service-ref配置了密碼加密方式,那麼資料庫中存入的使用者密碼為加密後的密碼,呼叫認證管理器會把資料庫查詢的密碼再使用加密方式加密一次,這樣密碼比對就會失敗。

解決這個問題,重寫認證管理器。

public Authentication authenticate(Authentication auth)

                throws AuthenticationException {

            String username = auth.getName();
            UserDetails userDetails = taUserDetailsService.loadUserByUsername(username);
            Object principal = userDetails;
            UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
            result.setDetails(auth.getDetails());
            return result;
        }
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(loginId, password);
        Authentication authentication = simpleAuthenticationManager.authenticate(authRequest);
        SecurityContextHolder.getContext().setAuthentication(authentication);

這樣,就不會使用配置的認證管理器,進行加密驗證了。