SpringSecurity應用篇

童話述說我的結局發表於2021-01-17

前面吹水原理吹了一篇幅了,現在講解下應用篇幅,前面說過,如果要用SpringSecurity的話要先匯入一個包

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

如果要更改預設的帳號密碼的話就配置如下

spring.security.user.name=admin
spring.security.user.password=admin

一、修改預設配置

通過前面的分析知道了修改預設一個是在配置檔案中修改,另一個是自定義SpringSecurity配置類,重寫配置類方法

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.inMemoryAuthentication()
                .withUser("admin")
                .password("{noop}admin")//不加密
                .authorities("ADMIN");

    }
    //自定義過濾器鏈
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //使用預設的過濾器鏈
        super.configure(httpSecurity);
    }

}

上面的配置雖然達到了修改帳號密碼及許可權的目的,但是有一個問題,那就是現在一切都是寫死的,而在真實環境中這些資料來源都是資料庫,所以如果想要了解怎麼從資料庫中動態獲取使用者資訊,那就要先從認證的原始碼進行分析起,有上篇的原碼篇說明,可以很清楚的知道認證的過濾器是UsernamePasswordAuthenticationFilter類,從下面原始碼可以看到預設的表單傳遞過來帳號密碼這裡都有接收,這個認證的過濾器他繼承了AbstractAuthenticationProcessingFilter這個過濾器

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    // ~ Methods
    // ========================================================================================================

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

    /**
     * Enables subclasses to override the composition of the password, such as by
     * including additional values and a separator.
     * <p>
     * This might be used for example if a postcode/zipcode was required in addition to
     * the password. A delimiter such as a pipe (|) should be used to separate the
     * password and extended value(s). The <code>AuthenticationDao</code> will need to
     * generate the expected password in a corresponding manner.
     * </p>
     *
     * @param request so that request attributes can be retrieved
     *
     * @return the password that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    /**
     * Enables subclasses to override the composition of the username, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     *
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    /**
     * Provided so that subclasses may configure what is put into the authentication
     * request's details property.
     *
     * @param request that an authentication request is being created for
     * @param authRequest the authentication request object that should have its details
     * set
     */
    protected void setDetails(HttpServletRequest request,
            UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * Sets the parameter name which will be used to obtain the username from the login
     * request.
     *
     * @param usernameParameter the parameter name. Defaults to "username".
     */
    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login
     * request..
     *
     * @param passwordParameter the parameter name. Defaults to "password".
     */
    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this filter. If set to
     * true, and an authentication request is received which is not a POST request, an
     * exception will be raised immediately and authentication will not be attempted. The
     * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
     * authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return usernameParameter;
    }

    public final String getPasswordParameter() {
        return passwordParameter;
    }
}

進入AbstractAuthenticationProcessingFilter過濾器;裡面有個doFilter方法,具體的就看這個方法

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {
    // ~ Static fields/initializers
    // =====================================================================================

    // ~ Instance fields
    // ================================================================================================

    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationManager authenticationManager;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private RememberMeServices rememberMeServices = new NullRememberMeServices();

    private RequestMatcher requiresAuthenticationRequestMatcher;

    private boolean continueChainBeforeSuccessfulAuthentication = false;

    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();

    private boolean allowSessionCreation = true;

    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    // ~ Constructors
    // ===================================================================================================

    /**
     * @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
     */
    protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        setFilterProcessesUrl(defaultFilterProcessesUrl);
    }

    /**
     * Creates a new instance
     *
     * @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to
     * determine if authentication is required. Cannot be null.
     */
    protected AbstractAuthenticationProcessingFilter(
            RequestMatcher requiresAuthenticationRequestMatcher) {
        Assert.notNull(requiresAuthenticationRequestMatcher,
                "requiresAuthenticationRequestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
    }

    // ~ Methods
    // ========================================================================================================

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(authenticationManager, "authenticationManager must be specified");
    }

    /**
     * Invokes the
     * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
     * requiresAuthentication} method to determine whether the request is for
     * authentication and should be handled by this filter. If it is an authentication
     * request, the
     * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
     * attemptAuthentication} will be invoked to perform the authentication. There are
     * then three possible outcomes:
     * <ol>
     * <li>An <tt>Authentication</tt> object is returned. The configured
     * {@link SessionAuthenticationStrategy} will be invoked (to handle any
     * session-related behaviour such as creating a new session to protect against
     * session-fixation attacks) followed by the invocation of
     * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
     * method</li>
     * <li>An <tt>AuthenticationException</tt> occurs during authentication. The
     * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
     * unsuccessfulAuthentication} method will be invoked</li>
     * <li>Null is returned, indicating that the authentication process is incomplete. The
     * method will then return immediately, assuming that the subclass has done any
     * necessary work (such as redirects) to continue the authentication process. The
     * assumption is that a later request will be received by this method where the
     * returned <tt>Authentication</tt> object is not null.
     * </ol>
     */
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
//attemptAuthentication是過濾認證資訊的,這個方法是上層的抽象方法,是交給子類去實現的 authResult
= attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); } /** * Indicates whether this filter should attempt to process a login request for the * current invocation. * <p> * It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in <em>https://host/myapp/index.html;jsessionid=blah</em>) * before matching against the <code>filterProcessesUrl</code> property. * <p> * Subclasses may override for special requirements, such as Tapestry integration. * * @return <code>true</code> if the filter should attempt authentication, * <code>false</code> otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); } /** * Performs actual authentication. * <p> * The implementation should do one of the following: * <ol> * <li>Return a populated authentication token for the authenticated user, indicating * successful authentication</li> * <li>Return null, indicating that the authentication process is still in progress. * Before returning, the implementation should perform any additional work required to * complete the process.</li> * <li>Throw an <tt>AuthenticationException</tt> if the authentication process fails</li> * </ol> * * @param request from which to extract parameters and perform the authentication * @param response the response, which may be needed if the implementation has to do a * redirect as part of a multi-stage authentication process (such as OpenID). * * @return the authenticated user token, or null if authentication is incomplete. * * @throws AuthenticationException if authentication fails. */ public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException; /** * Default behaviour for successful authentication. * <ol> * <li>Sets the successful <tt>Authentication</tt> object on the * {@link SecurityContextHolder}</li> * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li> * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured * <tt>ApplicationEventPublisher</tt></li> * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li> * </ol> * * Subclasses can override this method to continue the {@link FilterChain} after * successful authentication. * @param request * @param response * @param chain * @param authResult the object returned from the <tt>attemptAuthentication</tt> * method. * @throws IOException * @throws ServletException */ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 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); } /** * Default behaviour for unsuccessful authentication. * <ol> * <li>Clears the {@link SecurityContextHolder}</li> * <li>Stores the exception in the session (if it exists or * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li> * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li> * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li> * </ol> */ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } rememberMeServices.loginFail(request, response); failureHandler.onAuthenticationFailure(request, response, failed); } protected AuthenticationManager getAuthenticationManager() { return authenticationManager; } public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * Sets the URL that determines if authentication is required * * @param filterProcessesUrl */ public void setFilterProcessesUrl(String filterProcessesUrl) { setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher( filterProcessesUrl)); } public final void setRequiresAuthenticationRequestMatcher( RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); this.requiresAuthenticationRequestMatcher = requestMatcher; } public RememberMeServices getRememberMeServices() { return rememberMeServices; } public void setRememberMeServices(RememberMeServices rememberMeServices) { Assert.notNull(rememberMeServices, "rememberMeServices cannot be null"); this.rememberMeServices = rememberMeServices; } /** * Indicates if the filter chain should be continued prior to delegation to * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * , which may be useful in certain environment (such as Tapestry applications). * Defaults to <code>false</code>. */ public void setContinueChainBeforeSuccessfulAuthentication( boolean continueChainBeforeSuccessfulAuthentication) { this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication; } public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } protected boolean getAllowSessionCreation() { return allowSessionCreation; } public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } /** * The session handling strategy which will be invoked immediately after an * authentication request is successfully processed by the * <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the * session identifier to prevent session fixation attacks. * * @param sessionStrategy the implementation to use. If not set a null implementation * is used. */ public void setSessionAuthenticationStrategy( SessionAuthenticationStrategy sessionStrategy) { this.sessionStrategy = sessionStrategy; } /** * Sets the strategy used to handle a successful authentication. By default a * {@link SavedRequestAwareAuthenticationSuccessHandler} is used. */ public void setAuthenticationSuccessHandler( AuthenticationSuccessHandler successHandler) { Assert.notNull(successHandler, "successHandler cannot be null"); this.successHandler = successHandler; } public void setAuthenticationFailureHandler( AuthenticationFailureHandler failureHandler) { Assert.notNull(failureHandler, "failureHandler cannot be null"); this.failureHandler = failureHandler; } protected AuthenticationSuccessHandler getSuccessHandler() { return successHandler; } protected AuthenticationFailureHandler getFailureHandler() { return failureHandler; } }

點選attemptAuthentication,進入UsernamePasswordAuthenticationFilter方法的attemptAuthentication類中;這裡面就是密碼驗證的邏輯了

    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(); //封裝到Token物件中 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //認證;認證是在AuthenticationManager中做的,實現是authenticate return this.getAuthenticationManager().authenticate(authRequest); }

點選authenticate,進入他的ProviderManager類中的authenticate方法中看認證的過程 ,這個類中定義了一個private List<AuthenticationProvider> providers = Collections.emptyList();屬性,表示認證方式有多種

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        //針對多種認證做迴圈,取出每一種provider
        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 | InternalAuthenticationServiceException 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 = parentResult = 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 = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { 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}")); } // If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }

 點選authenticate進入 AuthenticationProvider類,這是一個抽象類,所以要找authenticate的實現,這裡面它的實現是AbstractUserDetailsAuthenticationProvider;這裡面如果不懂為什麼就打斷點看類圖,進入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); }

點選進入DaoAuthenticationProvider類的retrieveUser方法中

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
//根據名稱去載入使用者物件 UserDetails loadedUser
= this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
點選看loadUserByUsername載入看了啥事,會發現下面只有一個介面然後一個loadUserByUsername實現
 
public interface UserDetailsService {
    // ~ Methods
    // ========================================================================================================

    /**
     * Locates the user based on the username. In the actual implementation, the search
     * may possibly be case sensitive, or case insensitive depending on how the
     * implementation instance is configured. In this case, the <code>UserDetails</code>
     * object that comes back may have a username that is of a different case than what
     * was actually requested..
     *
     * @param username the username identifying the user whose data is required.
     *
     * @return a fully populated user record (never <code>null</code>)
     *
     * @throws UsernameNotFoundException if the user could not be found or the user has no
     * GrantedAuthority
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

點選看他的眾多實現,會發現裡在面有一個InMemoryUserDetailsManager實現,

 

 點選進入 ,這個介面會實現驗證的邏輯

    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        UserDetails user = users.get(username.toLowerCase());

        if (user == null) {
            throw new UsernameNotFoundException(username);
        }

        return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), user.getAuthorities());
    }

 

到了這一步,其實就很明顯了,既然你實現 他的介面,如果我想實現從資料進行匹配的話,我實現你的介面不就完事了嗎,那麼我們如果要實現自定義的認證流程也只需要實現UserDetailsService介面重寫loadUserByUsernameInMemoryUserDetailsManager就可以了
public interface UserService  extends UserDetailsService {
}

 

import com.example.demo.user.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    /**
     * 自已定義的認證邏輯方法,如果不懂看我寫的原理篇
     *
     * @param username
     * @return
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        //保證許可權的集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        //生產中是從資料庫拿的
        SimpleGrantedAuthority auth = new SimpleGrantedAuthority("ROLE_ROOT");
        authorities.add(auth);
        UserDetails user = new User(username
                , "{noop}admin"
                , true
                , true
                , true
                , true
                , authorities);
        return user;
    }
}

最後在SpringSecurity的配置檔案中再修改一下寫活就可以了

import com.example.demo.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


   @Autowired
   private UserService userService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//         auth.inMemoryAuthentication()
//                .withUser("admin")
//                .password("{noop}admin")//不加密
//                .authorities("ADMIN");
        auth.userDetailsService(userService);
    }
    //自定義過濾器鏈
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //使用預設的過濾器鏈
        super.configure(httpSecurity);
    }

}

到這一步密碼的自定義就實現了,有興趣的可以自己啟動試下;這個過程將帳號密碼及許可權的獲取改成動態了,到這裡可能有的人會想,我們自定義的類只實現了帳號的驗證,但並沒有看到密碼的驗證,其實前面我已經提到了密碼的驗證了,程式碼回退到AbstractUserDetailsAuthenticationProvider方法的authenticate類,其中的 preAuthenticationChecks.check(user);就是認證檢查的前置處理

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

點選check進入UserDetailsChecker類看它的處理

public interface UserDetailsChecker {
    /**
     * Examines the User
     * @param toCheck the UserDetails instance whose status should be checked.
     */
    void check(UserDetails toCheck);
}

選擇它的AbstractUserDetailsAuthenticationProvider實現類,進入AbstractUserDetailsAuthenticationProvider類的check方法

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")); } //是否可用 if (!user.isEnabled()) { logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } //是否過期 if (!user.isAccountNonExpired()) { logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } }

這一步看完後再回退到AbstractUserDetailsAuthenticationProvider方法的authenticate類,前置處理完後的additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);就是密碼的效驗了;

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

點選additionalAuthenticationChecks進入DaoAuthenticationProvider類的additionalAuthenticationChecks方法看它是怎麼進行密碼效驗的

    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
//先判斷有沒有密碼
if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } //獲取憑證 String presentedPassword = authentication.getCredentials().toString(); //做密碼的匹配,這兩個密碼一個是表單提交的密碼一個是從資料庫查出來的密碼 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }

再看上面的matches方法,進入PasswordEncoder類

public interface PasswordEncoder {

    /**
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
     * greater hash combined with an 8-byte or greater randomly generated salt.
     */
    String encode(CharSequence rawPassword);

    /**
     * Verify the encoded password obtained from storage matches the submitted raw
     * password after it too is encoded. Returns true if the passwords match, false if
     * they do not. The stored password itself is never decoded.
     *
     * @param rawPassword the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return true if the raw password, after encoding, matches the encoded password from
     * storage
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);

    /**
     * Returns true if the encoded password should be encoded again for better security,
     * else false. The default implementation always returns false.
     * @param encodedPassword the encoded password to check
     * @return true if the encoded password should be encoded again for better security,
     * else false.
     */
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

 

 

    @Override
    public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
//判斷字首
if (rawPassword == null && prefixEncodedPassword == null) { return true; } String id = extractId(prefixEncodedPassword); PasswordEncoder delegate = this.idToPasswordEncoder.get(id); if (delegate == null) { return this.defaultPasswordEncoderForMatches .matches(rawPassword, prefixEncodedPassword); } String encodedPassword = extractEncodedPassword(prefixEncodedPassword); return delegate.matches(rawPassword, encodedPassword); }

二、加密

我們在實際生產中不可能用明文傳遞密碼,所以接下來要聊的就是加密嘍,在SpringSecurity的官網中他推薦的加密方式是BCryptPasswordEncoder方式進行加密,剛剛在上面截圖中也看到了,所以接著改下專案

import com.example.demo.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.annotation.Resource;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


   @Autowired
   private UserService userService;

   @Resource
   private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//         auth.inMemoryAuthentication()
//                .withUser("admin")
//                .password("{noop}admin")//不加密
//                .authorities("ADMIN");
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
    //自定義過濾器鏈
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //使用預設的過濾器鏈
        super.configure(httpSecurity);
    }

    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

然後UserServiceImpl裡面就不能用明文了,要用密文了

@Service
public class UserServiceImpl implements UserService {

    /**
     * 自已定義的認證邏輯方法,如果不懂看我寫的原理篇
     *
     * @param username
     * @return
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        //保證許可權的集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        //生產中是從資料庫拿的
        SimpleGrantedAuthority auth = new SimpleGrantedAuthority("ROLE_ROOT");
        authorities.add(auth);
        UserDetails user = new User(username
                , "$2a$10$YOWyHqvtg.gqrbiSTlYQx.nu2j0psWsrs/JIiuzav7IDX7r93WGIe"
                , true
                , true
                , true
                , true
                , authorities);
        return user;
    }
}

2.1、認證狀態判斷

在實際專案中因為使用者的不同操作,可能會給出不同的狀態,比如正常、凍結等。SpringSecurity也支援

 

 在User物件的屬性中定義如下

 

 

三、自定義認證頁面

好久沒寫前端程式碼了,為了偷懶,用thymeleaf來寫吧,先導包

 

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

搞個登入頁面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登入管理</h1>

    <form th:action="@{/login.do}" method="post">
        賬號:<input type="text" name="username"><br>
        密碼:<input type="password" name="password"><br>
        <input type="submit" value="登入"><br>
    </form>
</body>
</html>

再搞個異常頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>錯誤頁面</h1>
</body>
</html>
@Controller
public class UserController {
    @GetMapping("/login.html")
    public String loginPage(){
         
        return "/login.html";
    }

    @GetMapping("/index.html")
    public String index(){
        return "/index.html";
    }

    @GetMapping("/")
    public String basePage(){
        return "/index.html";
    }

    @GetMapping("/error.html")
    public String error(){
        return "/error.html";
    }
}

要使用自定義頁面的話就要修改掉預設的頁面,修改WebSecurityConfig類

 

相關文章