文章目录

  • 1. AbstractAuthenticationProcessingFilter 过滤器
  • 2. UsernamePasswordAuthenticationFilter 过滤器
  • 3. 自定义过滤器实现 Json 格式登录


SpringSecurity 中默认的是表单登录格式,即用户在表单中输入用户名和密码进行登录,登录参数的提取是在 UsernamePasswordAuthenticationFilter 过滤器中完成的,UsernamePasswordAuthenticationFilter 是AbstractAuthenticationProcessingFilter 针对使用用户名和密码进行身份认证而定制化的一个过滤器。其添加是在调用http.formLogin() 时作用,默认的登录请求 pattern 为 “/login”,并且为 POST 请求。当我们登录的时候,也就是匹配到loginProcessingUrl,这个过滤器就会委托认证管理器 authenticationManager 来验证登录。

UsernamePasswordAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter

1. AbstractAuthenticationProcessingFilter 过滤器

AbstractAuthenticationProcessingFilter 是一个抽象类,主要的功能是身份认证。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都继承了 AbstractAuthenticationProcessingFilter ,并重写了方法 attemptAuthentication 进行身份认证。

AbstractAuthenticationProcessingFilter 源码:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    
    //事件发布管理器
    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    // 认证管理器,定义了SpringSecurity如何进行认证操作
    // 认证成功后会返回一个Authentication对象,这个对象会被设置到SecurityContextHolder中
    private AuthenticationManager authenticationManager;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    // 如果用户开启了类似“记住我”之类的免密码登录,RememberMeServices来进行管理。
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    // 请求匹配器,定义了match()方法,匹配请求HttpServletRequest是否符合定义的规则
    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();
    
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        // 判断是否需要认证
        if (!this.requiresAuthentication(request, response)) {
            // 如果不需要认证,继续执行下一个过滤器
            chain.doFilter(request, response);
        } else {
            Authentication authResult;
            try {
                // 认证处理,该方法需要子类去重写
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }
                // 身份认证成功,保存session
                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                // 认证失败的处理逻辑
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                // 认证失败的处理逻辑
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            // 认证成功的处理逻辑
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

     // 判断该filter是否需要处理该次请求,即请求的路径和该filter配置的要处理的url是否匹配
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return this.requiresAuthenticationRequestMatcher.matches(request);
    }

    // 这个方法的目的很明确,就是需要子类提供身份认证的具体实现。
    // 子类根据 HttpServletRequest 等信息进行身份认证,并返回 Authentication 对象、 null、异常 
    // 分别表示认证成功返回的身份认证信息、需要其他 Filter 继续进行身份认证、认证失败。
    public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;

    // 认证成功的处理逻辑
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 将认证成功的用户信息保存到 SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authResult);
        // 处理记住我逻辑
        this.rememberMeServices.loginSuccess(request, response, authResult);
        // 发布时间,即发布认证成功消息,供其他的bean接收和处理
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
		// 认证成功后后续处理
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    // 认证失败的处理逻辑
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }
}
2. UsernamePasswordAuthenticationFilter 过滤器

1、页面输入用户名和密码;

2、登录请求被UsernamePasswordAuthenticationFilter过滤器拦截,开始进行登录认证;

3、UsernamePasswordAuthenticationFilter过滤器会获取用户表单输入的用户名和棉麻,并创建一个未认证的UsernamePasswordAuthenticationToken,然后调用认证管理器AuthenticationManager完成认证功能;

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    // 表单提交的username的name属性值
    private String usernameParameter = "username";
    // 表单提交的password的name属性值
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        // 默认处理/login请求且为Post方式
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            // 获取用户名
            String username = this.obtainUsername(request);
            //获取密码
            String password = this.obtainPassword(request);
            
            // 参数校验
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            // 将用户的用户名和密码封装成 UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            // 使用AuthenticationManager认证管理器完成认证功能
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    // 从request中获取用户提交的密码
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    // 从request中获取用户提交的用户名
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

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

4、AuthenticationManager不直接进行登录验证,而是使用DaoAuthenticationProvider进行验证,DaoAuthenticationProvider调用UserDetailsService(自定义)对象的loadUserByUsername方法获取用户信息,然后对获得的对象进行一系列的检查(自定义),包括预检查、附加检查、后检查。预检查检查用户是否被冻结、是否启用、是否过期。附加检查进行用户名密码验证。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }

    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }

    }
}

5、登陆成功后将验证过的用户信息存储在SecurityContext中,然后调用登录成功处理器(自定义)。

// 认证成功的处理逻辑
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    // 将认证成功的用户信息保存到 SecurityContextHolder
    SecurityContextHolder.getContext().setAuthentication(authResult);
    // 处理记住我逻辑
    this.rememberMeServices.loginSuccess(request, response, authResult);
    // 发布时间,即发布认证成功消息,供其他的bean接收和处理
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    // 认证成功后后续处理
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
3. 自定义过滤器实现 Json 格式登录

在实际项目中,我们可能通过 json 格式来传递参数,这就需要我们自定义登录过滤器链来实现。登录参数的提取是在UsernamePasswordAuthenticationFilter过滤器中完成的,如果要使用 Json 格式登录,只要模仿UsernamePasswordAuthenticationFilter过滤器自定义自己的过滤器,再将自定义的额过滤器放到UsernamePasswordAuthenticationFilter过滤器所在的位置即可。

定义一个LoginFilter 继承自UsernamePasswordAuthenticationFilter:

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 确保进入过滤器中的请求时Post请求
        if(!request.getMethod().equals("POST")){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)
                || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)){
            Map<String,String> userInfo = new HashMap<>();
            try {
                // 将输入流转为Map对象
                userInfo = objectMapper.readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                // 从Map对象中分别提取用户名和密码
                // 构造成UsernamePasswordAuthenticationToken对象
                UsernamePasswordAuthenticationToken authRequest
                        = new UsernamePasswordAuthenticationToken(username,password);
                setDetails(request,authRequest);
                // 调用AuthenticationManager的 authticate()方法完成认证操作
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //如果不是json格式,那么按照父类的表单登录逻辑处理
        return super.attemptAuthentication(request,response);
    }
}

LoginFilter 定义完后,需要将其添加到SpringSecurity过滤器链中,代码如下:

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zhansan")
                .password("{noop}123")
                .roles("admin");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        // 设置认证管理器
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        // 自定义认证成功的处理逻辑,以json格式写回浏览器
        loginFilter.setAuthenticationSuccessHandler(((httpServletRequest, httpServletResponse, authentication) -> {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(new ObjectMapper().writeValueAsString(authentication));
        }));
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
        // 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

使用Postman测试 json 格式登录:

{"username":"zhangsan","password":"123"}