• SpringSecurity - 自定义过滤器使用 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    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);
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    使用Postman测试 json 格式登录:

    {"username":"zhangsan","password":"123"}
    
    • 1
  • 相关阅读:
    [附源码]SSM计算机毕业设计教学辅助系统JAVA
    【第六部分 | JavaScript高级】2:函数高级
    SpringBoot:MVC自动配置原理
    java毕业设计车辆保险管理系统Mybatis+系统+数据库+调试部署
    常见的中间件都在解决什么问题?
    接口测试到底怎么做,5分钟时间看完这篇文章彻底搞清楚
    Linux简介
    FPGA通信—千兆网(UDP)软件设计
    【开学季Blog】再见过去,你好未来!记得做好自己呀!
    【华为上机真题 2021】分子弹
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126512550