Spring Security Web 5.1.2 源码解析 — UsernamePasswordAuthenticationFilter

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】
免费领取10G资料包与项目实战视频资料

概述

该过滤器会拦截用户请求,看它是否是一个来自用户名/密码表单登录页面提交的用户登录认证请求,缺省使用的匹配模式是:POST /login。缺省情况下,如果是用户登录认证请求,该请求就不会在整个filter chain中继续传递了,而是会被当前过滤器处理并进入响应用户阶段。

具体用户登录认证处理逻辑是这样的,它会调用所指定的AuthenticationManager认证所提交的用户名/密码。

如果认证成功,则会 :

  1. 调用所设置的SessionAuthenticationStrategy会话认证策略;

    针对Servlet 3.1+,缺省所使用的SessionAuthenticationStrategy是一个ChangeSessionIdAuthenticationStrategyCsrfAuthenticationStrategy组合。ChangeSessionIdAuthenticationStrategy会为登录的用户创建一个新的session,而CsrfAuthenticationStrategy会创建新的csrf token用于CSRF保护。

  2. 经过完全认证的Authentication对象设置到SecurityContextHolder中的SecurityContext上;
  3. 如果请求要求了Remember Me,进行相应记录;
  4. 发布事件InteractiveAuthenticationSuccessEvent;
  5. 获取并跳转到目标跳转页面;

    缺省情况下,该跳转策略是SavedRequestAwareAuthenticationSuccessHandler

    • 如果有保存的请求,则获取保存的请求,跳转到相应的请求地址;

      一般在未登录用户直接访问受保护页面时会出现该情况:先被跳转到登录页面,登录完成过后再被跳转到原始请求页面

    • alwaysUseDefaultTargetUrltrue则总是会跳到指定的defaultTargetUrl;

      注意: defaultTargetUrl 也是可以设置的,如果不设置,其值缺省为/

    • alwaysUseDefaultTargetUrlfalse

      • 看请求参数中是否含有名称为配置参数targetUrlParameter值的参数,如果有,跳转到它定义的地址;
      • 否则如果指定了useReferer,尝试使用请求头部Referer作为目标跳转地址;
      • 否则使用defaultTargetUrl作为目标跳转地址;

源代码解析

    package org.springframework.security.web.authentication;

    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.util.Assert;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    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() {
            // 缺省匹配用户请求 POST /login,认为该请求是用户名/密码表单登录验证请求
            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 = "";
            }

            // 注意,这里对用户名做了trim操作,一般理解,就是去除了前后的空格
            username = username.trim();

            // 根据用户提供的用户名/密码信息构建一个认证token
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);

            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);

            // 交给 authenticationManager执行真正的用户身份认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }

        /** * 从请求参数中获取密码 * * Enables subclasses to override the composition of the password, such as by * including additional values and a separator. * * 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 AuthenticationDao will need to * generate the expected password in a corresponding manner. * * * @param request so that request attributes can be retrieved * * @return the password that will be presented in the Authentication * request token to the AuthenticationManager */
        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 Authentication * request token to the AuthenticationManager */
        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. * 指定从用户名/密码登录认证表单中获取用户名时使用的属性名称,缺省为 username * @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.. * 指定从用户名/密码登录认证表单中获取用户名时使用的属性名称,缺省为 password * @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 * unsuccessfulAuthentication() method will be called as if handling a failed * authentication. * * 设置是否仅仅接受HTTP POST用户名/密码登录验证表单请求,缺省为true,表示必须使用HTTP POST。 * * Defaults to true 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;
        }
    }

UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter:

    package org.springframework.security.web.authentication;

    import java.io.IOException;

    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.ApplicationEventPublisherAware;
    import org.springframework.context.MessageSource;
    import org.springframework.context.MessageSourceAware;
    import org.springframework.context.support.MessageSourceAccessor;
    import org.springframework.security.authentication.AuthenticationDetailsSource;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.SpringSecurityMessageSource;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.security.web.util.matcher.RequestMatcher;
    import org.springframework.util.Assert;
    import org.springframework.web.filter.GenericFilterBean;

    /** * Abstract processor of browser-based HTTP-based authentication requests. * 基于浏览器和HTTP的认证请求的处理抽象。 * * Authentication Process 认证过程 * * The filter requires that you set the authenticationManager property. An * AuthenticationManager is required to process the authentication request tokens * created by implementing classes. * 该过滤器执行认证需要一个authenticationManager,这个AuthenticationManager用来针对 * 所创建的认证请求token做真正的用户身份认证动作。 * * This filter will intercept a request and attempt to perform authentication from that * request if the request matches the * #setRequiresAuthenticationRequestMatcher(RequestMatcher). * 该过滤器拦截请求识别当前请求是否一个用户名/密码表单认证请求的匹配方法是通过方法 * #setRequiresAuthenticationRequestMatcher(RequestMatcher)指定的一个RequestMatcher。 * * Authentication is performed by the * #attemptAuthentication(HttpServletRequest, HttpServletResponse) * method, which must be implemented by subclasses. * 认证动作由方法#attemptAuthentication(HttpServletRequest, HttpServletResponse)完成, * 该方法由子类实现。上面的UsernamePasswordAuthenticationFilter就提供了该方法的实现。 * * Authentication Success 认证成功 * * If authentication is successful, the resulting Authentication object will be * placed into the SecurityContext for the current thread, which is * guaranteed to have already been created by an earlier filter. * 认证成功时,结果认证对象Authentication会被放到一个SecurityContext对象中,然后保存在处理 * 当前请求的线程中。 * * The configured #setAuthenticationSuccessHandler(AuthenticationSuccessHandler) * will then be called to take the redirect to the * appropriate destination after a successful login. The default behaviour is implemented * in a SavedRequestAwareAuthenticationSuccessHandler which will make use of any * DefaultSavedRequest set by the ExceptionTranslationFilter and * redirect the user to the URL contained therein. Otherwise it will redirect to the * webapp root "/". You can customize this behaviour by injecting a differently configured * instance of this class, or by using a different implementation. * 然后通过#setAuthenticationSuccessHandler(AuthenticationSuccessHandler)所设置的 * AuthenticationSuccessHandler会被调用,从而页面被跳转到所配置的成功登录页面。 * * Authentication Failure 认证失败 * * If authentication fails, it will delegate to the configured * AuthenticationFailureHandler to allow the failure information to be conveyed to * the client. The default implementation is SimpleUrlAuthenticationFailureHandler * , which sends a 401 error code to the client. It may also be configured with a failure * URL as an alternative. Again you can inject whatever behaviour you require here. * 如果认证失败,该过滤器会代理给AuthenticationFailureHandler将失败信息传回给客户。缺省实现是 * 一个SimpleUrlAuthenticationFailureHandler,它会发送一个HTTP 401代码给客户端。 * * Event Publication 事件发布 * * If authentication is successful, an InteractiveAuthenticationSuccessEvent will * be published via the application context. No events will be published if authentication * was unsuccessful, because this would generally be recorded via an * AuthenticationManager-specific application event. * 认证成功时一个InteractiveAuthenticationSuccessEvent事件会发布到应用上下文。认证不成功 * 则不会发布任何事件。 * * Session Authentication * * The class has an optional SessionAuthenticationStrategy which will be invoked * immediately after a successful call to attemptAuthentication(). Different * implementations * #setSessionAuthenticationStrategy(SessionAuthenticationStrategy) can be * injected to enable things like session-fixation attack prevention or to control the * number of simultaneous sessions a principal may have. * * @author Ben Alex * @author Luke Taylor */
    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;

        // 登录认证成功时是否继续执行filter chain,缺省为false,该属性安全配置阶段会重新指定,
        // 但安全配置阶段缺省使用的值也是false,表示登录认证成功时不继续执行filter chain,而是
        // 由该页面直接进入响应用户阶段
        private boolean continueChainBeforeSuccessfulAuthentication = false;

        // session 认证策略
        // 安全配置中缺省是一个 CompositeSessionAuthenticationStrategy 对象,应用了组合模式,组合一些其他的
        // session 认证策略实现,比如针对Servlet 3.1+,缺省会是 ChangeSessionIdAuthenticationStrategy跟
        // CsrfAuthenticationStrategy组合
        // 这里虽然初始化为NullAuthenticatedSessionStrategy,但它会被安全配置中的值覆盖
        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");
        }

        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {

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

            if (!requiresAuthentication(request, response)) {
                // 检查当前请求是否是一个用户名/密码登录表单请求,如果不是,则继续执行filter chain
                // 的其他部分
                chain.doFilter(request, response);

                return;
            }

            // 下面是检测到这是一个用户名/密码登录表单请求时的处理逻辑
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }

            Authentication authResult;

            try {
                // 交给 AuthenticationManger 执行相应的认证
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                //针对Servlet 3.1+,缺省所使用的SessionAuthenticationStrategy会是一个
                //ChangeSessionIdAuthenticationStrategy和CsrfAuthenticationStrategy组合。
                //ChangeSessionIdAuthenticationStrategy会为登录的用户创建一个新的session,
                //而CsrfAuthenticationStrategy会创建新的csrf token用于CSRF保护。
                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
            // 认证成功,如果continueChainBeforeSuccessfulAuthentication为true,
            // (continueChainBeforeSuccessfulAuthentication缺省为false)
            // 继续执行filter chain的其他部分
            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. * * It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in http://host/myapp/index.html;jsessionid=blah) * before matching against the filterProcessesUrl property. * * Subclasses may override for special requirements, such as Tapestry integration. * * @return true if the filter should attempt authentication, * false otherwise. */
        protected boolean requiresAuthentication(HttpServletRequest request,
                HttpServletResponse response) {
            return requiresAuthenticationRequestMatcher.matches(request);
        }

        /** * Performs actual authentication. 执行真正的认证 * * The implementation should do one of the following: * 会是以下三种情况之一: * * 1.Return a populated authentication token for the authenticated user, indicating * successful authentication 认证成功,填充认证了的用户的其他信息到authentication token并返回之 * 2.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.返回null表示认证仍在进行中。 * 3.Throw an AuthenticationException if the authentication process fails。抛出一个 * 异常AuthenticationException表示认证失败。 * * * @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.认证成功时的缺省行为。 * * 1. Sets the successful Authentication object on the SecurityContextHolder * 2. Informs the configured RememberMeServices of the successful login * 3.Fires an InteractiveAuthenticationSuccessEvent via the configured * ApplicationEventPublisher * 4.Delegates additional behaviour to the AuthenticationSuccessHandler. * * * Subclasses can override this method to continue the FilterChain after * successful authentication. * @param request * @param response * @param chain * @param authResult the object returned from the attemptAuthentication * 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. * 认证失败时的缺省行为 * * 1.Clears the SecurityContextHolder * 2.Stores the exception in the session (if it exists or * allowSesssionCreation is set to true) * 3.Informs the configured RememberMeServices of the failed login * 4.Delegates additional behaviour to the AuthenticationFailureHandler. * */
        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 * #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication) * , which may be useful in certain environment (such as Tapestry applications). * Defaults to false. */
        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 * AuthenticationManager. 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 * 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;
        }
    }

参考文章


来源:http://ddrv.cn/a/88268

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Spring Security Web 5.1.2 源码解析 — UsernamePasswordAuthenticationFilter

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏