zoukankan      html  css  js  c++  java
  • Spring Securtiy 认证流程(源码分析)

    当用 Spring Security 框架进行认证时,你可能会遇到这样的问题:

    你输入的用户名或密码不管是空还是错误,它的错误信息都是 Bad credentials。

    那么如果你想根据不同的情况给出相应的错误提示该怎么办呢?

    这个时候我们只有了解 Spring Securiy 认证的流程才能知道如何修改代码。

    好啦,来看下面的例子,大部分人的 WebSecurityConfig 的 configure 代码都类似于下:

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // TODO Auto-generated method stub
            http
            .authorizeRequests()
            .anyRequest().permitAll()
            .and()
            .formLogin().loginPage("/signin")
            .usernameParameter("username")
            .passwordParameter("password")
            .loginProcessingUrl("/signin")
            .and()
            .csrf().disable();
        }

    相信以上代码大家都知道什么意思:任何请求信息都允许,也就是不需要身份认证。

    登录页面请求为 /signin,用户名和密码参数的name属性分别是 username,password。登录页面 form 的 action 请求为 /signin。

    当然这个 action 不必和登录页面请求一样。最后的那个是禁止跨站请求伪造。

    这段代码和登录认证联系较大的应该是从 loginPage() 到 loginProcessingUrl() 里的方法。

    咱先从 loginPage 看起,鼠标左键拖动覆盖 loginPage,然后右键 Open Declaration 就进入到了 FormLoginConfigurer 类。

    这个类里值得注意的方法有两个:构造方法和 loginPage 方法。

        public FormLoginConfigurer() {
            super(new UsernamePasswordAuthenticationFilter(), null);
            usernameParameter("username");
            passwordParameter("password");
        }
    
        public FormLoginConfigurer<H> loginPage(String loginPage) {
            return super.loginPage(loginPage);
        }

    构造方法中使用了一个用户名密码认证过滤器类,这一看就和认证有关系。

    loginPage 方法大家可以自行按照这个步骤查看,现在直接看 UsernamePasswordAuthenticationFilter 类。

        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;
    
       public UsernamePasswordAuthenticationFilter() {
            super(new AntPathRequestMatcher("/login", "POST"));
        }
    
       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);
        
    setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }

     这只是其中一部分代码,其他的可以自己看。该类中定义的两个字符串和构造方法定义了默认的登录方式。

    登录 action 请求为以 POST 方式的 /login,用户名及密码分别以 username,password 属性值获取。

    该类的父类的父类 GenericFilterBean 实现了 InitializingBean 接口,也就是会初始化为一个 Bean。

    当看到 attemptAuthentication 时,就知道他是认证的方法啦。

    这里咱直接看到 new UsernamePasswordAuthenticationToken(username, password);

        public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            setAuthenticated(false);
        }

    从这里可以知道,它把用户名和密码分别存在了 principal,credentials 里。

    现在我们只需要记住登录信息存在了 authRequest 里。现在来看下setDetails,虽然我不感兴趣。

        protected void setDetails(HttpServletRequest request,
                UsernamePasswordAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }

    它调用了一个 buildDetails 方法,实际上是调用的:(追根溯源可以看到)

        
    /**
      * Records the remote address and will also set the session Id if a session already
      * exists (it won't create one).
      *
      * @param request that the authentication request was received from
      */
    public WebAuthenticationDetails(HttpServletRequest request) { this.remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession(false); this.sessionId = (session != null) ? session.getId() : null; }

    从源码注释可以看到,它是记录远程地址并且会设置一个会话 ID,这里我们不管它了。

    直接看这一句:return this.getAuthenticationManager().authenticate(authRequest);

    它调用的是一个实现了 AuthenticationManager 接口的类的 authenticate 方法。

    从源码中我们找不到它用的是哪个实现类,网上说是 ProviderManager 类,我们来看一下该类。

    public class ProviderManager implements AuthenticationManager, MessageSourceAware,
            InitializingBean {
      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();
    
            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 (InternalAuthenticationServiceException e) {
                    prepareException(e, authentication);
                    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 than 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 than 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 方法,从类的声明可以看出来它也会初始化为一个 Bean,咱找不到很正常对吧。

    authenticate 方法会遍历所有的 AuthenticationProvider ,然后调用 provider 的 authenticate 方法。

    如果认证结果不为空的话将会保存到 result 中,并且擦除认证信息再返回 result。

    为空的话一般是没有提供 AuthenticationProvider,会报 ProviderNotFoundException 错误。

    现在我们来看下 provider 的 authenticate 方法。

        @Bean
        public AuthenticationProvider authenticationProvider() {
            DaoAuthenticationProvider provider = new CustomAuthenticationProvider();
            provider.setMessageSource(messageSource);
            provider.setUserDetailsService(userService);
            provider.setPasswordEncoder(new BCryptPasswordEncoder());
            return provider;
        }

    这个是我写的一个 AuthenticationProvider,只不过我重写了一个类继承了 DaoAuthenticationProvider。

    这里我们来看 DaoAuthenticationProvider 类:(这个类里面并没有发现  authenticate 方法,那先从它的父类找)

    父类是 AbstractUserDetailsAuthenticationProvider,它也实现了 InitializingBean 接口,也是初始化为一个 Bean。

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

     在这段代码中可以知道:如果 authentication.getPrincipal() 为空的话,username 将会为 NONE_PROVIDED。

    不为空的话将会得到 authentication.getPrincipal(),也就是用户名,只是这种类型不是 String 类型,但可以强制转换。

    代码中是 authentication.getName(),这种和上面基本一样,只不过该类型是 String 类型的。

    然后定义一个 user,先尝试从缓存中获取 user,没获取到的话就通过 retrieveUser 获取。

    该类中 retrieveUser 是一个抽象方法,我们现在来看 DaoAuthenticationProvider 类里的方法。

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

    从代码中可以看到是通过我们之前写的 UserDetailsService 方法获取用户。

    接下来我们看后面的代码,这部分异常代码我们等会再看。

    try {
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }

    这两句代码是对用户进行检查的,第一行代码调用的其实是这部分的:

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

    可以看到并不是检查密码的,只是对用户状态进行检查。那么我们不管它了,看下一行代码:

    @SuppressWarnings("deprecation")
        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"));
            }
        }

    这里有个获取密码的操作:authentication.getCredentials()。

    然后如果密码不为空的话就通过 passwordEncoder.matches(presentedPassword, userDetails.getPassword() 检查是否匹配。

    如果匹配成功的话,嗯,这部分结束了,我们回到 AbstractUserDetailsAuthenticationProvider 类里的 authenticate 方法。

    return createSuccessAuthentication(principalToReturn, authentication, user);

    它会返回一个创建成功认证方法的返回值。这里我们就不管了。

    现在我们先回到AbstractUserDetailsAuthenticationProvider 类的错误处理上:

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

    这个是用户找不到引起的错误,我们看下 messages.getMessage():

        public String getMessage(String code, String defaultMessage) {
            String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale());
            return (msg != null ? msg : "");
        }

    再来看下这个里面的 getMessage():

    它是一个接口类里的方法:根据 code 返回 messageSource 里的字符串,如果不存在这个 code,就返回 defaultMessage。

    既然是个接口类,那我们看下它的实现类,回到 messageSource,查看一下它:

    public class SpringSecurityMessageSource extends ResourceBundleMessageSource {
        // ~ Constructors
        // ===================================================================================================
    
        public SpringSecurityMessageSource() {
            setBasename("org.springframework.security.messages");
        }
    
        // ~ Methods
        // ========================================================================================================
    
        public static MessageSourceAccessor getAccessor() {
            return new MessageSourceAccessor(new SpringSecurityMessageSource());
        }
    }

    原来是从这个路径里找数据源。

    其他的错误处理也是一样,这里就省略了。那我们如何获取错误信息呢?

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // TODO Auto-generated method stub
            http
            .authorizeRequests()
            .anyRequest().permitAll()
            .and()
            .formLogin().loginPage("/signin")
            .usernameParameter("username")
            .passwordParameter("password")
            .loginProcessingUrl("/signin")
            .failureHandler(authenticationFailureHandler)
            .and()
            .csrf().disable();

    看到那个 failureHandler 没,这个是登录失败处理器,这里加上只是看一下里面源码:AbstractAuthenticationFilterConfigurer

        /**
         * Specifies the {@link AuthenticationFailureHandler} to use when authentication
         * fails. The default is redirecting to "/login?error" using
         * {@link SimpleUrlAuthenticationFailureHandler}
         *
         * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
         * when authentication fails.
         * @return the {@link FormLoginConfigurer} for additional customization
         */
        public final T failureHandler(
                AuthenticationFailureHandler authenticationFailureHandler) {
            this.failureUrl = null;
            this.failureHandler = authenticationFailureHandler;
            return getSelf();
        }

    从注释中可以看出默认的失败处理器是 SimpleUrlAuthenticationFailureHandler:

        public void onAuthenticationFailure(HttpServletRequest request,
                HttpServletResponse response, AuthenticationException exception)
                throws IOException, ServletException {
    
            if (defaultFailureUrl == null) {
                logger.debug("No failure URL set, sending 401 Unauthorized error");
    
                response.sendError(HttpStatus.UNAUTHORIZED.value(),
                    HttpStatus.UNAUTHORIZED.getReasonPhrase());
            }
            else {
                saveException(request, exception);
    
                if (forwardToDestination) {
                    logger.debug("Forwarding to " + defaultFailureUrl);
    
                    request.getRequestDispatcher(defaultFailureUrl)
                            .forward(request, response);
                }
                else {
                    logger.debug("Redirecting to " + defaultFailureUrl);
                    redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
                }
            }
        }

    因为默认的 defaultFailureUrl 为 /login?error,从 AbstractAuthenticationFilterConfigurer 类里可以看出来。

    登录失败后,会调用 saveException(request, exception); 保存错误信息。

    protected final void saveException(HttpServletRequest request,
                AuthenticationException exception) {
            if (forwardToDestination) {
                request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
            }
            else {
                HttpSession session = request.getSession(false);
    
                if (session != null || allowSessionCreation) {
                    request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
                            exception);
                }
            }
        }

    由于该类中 forwardToDestination 为 false,它将执行 else 里的语句。

    将错误信息保存到会话的 WebAttributes.AUTHENTICATION_EXCEPTION 属性中:

    public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";

    所有我们可以通过会话的这个属性来获取错误信息。(thymeleaf)

    (注意:signin.html 不能放在 static 目录下,不然获取不到错误信息。)

    <p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>

    好啦,都介绍完了,可以看下我的 CustomAuthenticationProvider:

    package security.config;
    
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.util.Assert;
    
    public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
        
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails,
                UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            // TODO Auto-generated method stub
    
            String presentedPassword = authentication.getCredentials().toString();
            if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");
    
                throw new BadCredentialsException(messages.getMessage(
                        "UNameOrPwdIsError","Username or Password is not correct"));
            }
        }
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // TODO Auto-generated method stub
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    () -> messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));
    
            if("".equals(authentication.getPrincipal())) {
                throw new BadCredentialsException(messages.getMessage(
                        "UsernameIsNull","Username cannot be empty"));
            }
            if("".equals(authentication.getCredentials())) {
                throw new BadCredentialsException(messages.getMessage(
                        "PasswordIsNull","Password cannot be empty"));
            }
            
            String username = (String) authentication.getPrincipal();
            boolean cacheWasUsed = true;
            UserDetails user = this.getUserCache().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(
                                "UNameOrPwdIsError","Username or Password is not correct"));
                    }
                    else {
                        throw notFound;
                    }
                }
                Assert.notNull(user,
                        "retrieveUser returned null - a violation of the interface contract");
            }
            try {
                getPreAuthenticationChecks().check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    cacheWasUsed = false;
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                    getPreAuthenticationChecks().check(user);
                    additionalAuthenticationChecks(user,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                else {
                    throw exception;
                }
            }
    
            getPostAuthenticationChecks().check(user);
    
            if (!cacheWasUsed) {
                this.getUserCache().putUserInCache(user);
            }
    
            Object principalToReturn = user;
    
            if (isForcePrincipalAsString()) {
                principalToReturn = user.getUsername();
            }
    
            return createSuccessAuthentication(principalToReturn, authentication, user);
        }
        
    
    }

    这里值得注意的是 "".equals(authentication.getPrincipal()),"".equals(authentication.getCredentials())

    因为如果按照那个 AbstractUserDetailsAuthenticationProvider 类来写的话,发现这一步永不为 null。

    我通过加入代码 System.out.println(username);  才知道的,应该是个坑吧。

    项目代码可供大家参考:

    链接:https://pan.baidu.com/s/13fc6P9NV49aRRBctr3MjNQ
    提取码:4qgu

  • 相关阅读:
    React Hooks 全解(一)
    Google搜索技巧
    #!/usr/bin/python3 和 #!/usr/bin/env python3 的区别
    Python函数
    Python程序代码阅读
    画个爱心向你表白
    自学需要注意的点
    Python文件操作
    国内加速访问GitHub
    (九) -前端-异步编程
  • 原文地址:https://www.cnblogs.com/M-Anonymous/p/12003968.html
Copyright © 2011-2022 走看看