Java框架快速入门: Spring Security+OAuth2之自定义认证过滤器实现JSON登录

概述

在前后端分离的开发模式下,传统的表单提交(Form Login)已经无法满足无状态、JSON交互的需求。Spring Security 默认的 UsernamePasswordAuthenticationFilterHttpServletRequest 中提取 parameter 来获取用户名和密码,这显然不适用于 application/json 类型的请求体。

本文将深入分析 UsernamePasswordAuthenticationFilter 的源码逻辑,并手把手带你实现一个自定义的 RestAuthenticationFilter,使其支持从 JSON 中读取凭证。同时,我们将讨论如何将这个过滤器注册到 Spring Security 的过滤器链中,并处理 CSRF 防护带来的问题,最终实现一个纯 JSON 交互的认证端点。

核心概念与代码结构

  • 核心概念Filter 生命周期、AuthenticationManagerAuthentication 对象、ObjectMapper 序列化、SecurityFilterChain 配置。

  • 涉及组件

    • UsernamePasswordAuthenticationFilter(默认表单过滤器)
    • RestAuthenticationFilter(自定义 JSON 过滤器)
    • AuthenticationSuccessHandlerAuthenticationFailureHandler(自定义响应处理器)
    • HttpSecurity 配置与 addFilterAt / addFilterBefore 注册方式。
  • 项目结构预览

    dir 复制代码
    src/main/java/com/example/demo/
    ├── config
    │   └── SecurityConfig.java          # Spring Security 核心配置
    ├── filter
    │   └── RestAuthenticationFilter.java # 自定义 JSON 认证过滤器
    ├── handler
    │   ├── JsonAuthenticationSuccessHandler.java
    │   └── JsonAuthenticationFailureHandler.java
    └── DemoApplication.java

源码分析:默认的表单认证机制

在着手自定义之前,我们先深入 Spring Security 的源码,看看默认的 UsernamePasswordAuthenticationFilter 是如何工作的。这为我们的"依葫芦画瓢"提供了理论基础。

attemptAuthentication 方法中,核心流程如下:

  1. 请求方法校验 :若非 POST 请求,直接抛出异常。
  2. 提取凭证 :通过 request.getParameter(usernameParameter)request.getParameter(passwordParameter) 从表单参数中获取用户名和密码。
  3. 封装 Token :构造 UsernamePasswordAuthenticationToken 对象。
  4. 设置详情 :调用 setDetails(request, token) 存入 IP、Session 等信息。
  5. 认证委托 :调用 AuthenticationManager.authenticate(token) 进行实际的认证逻辑。

#mermaid-svg-ra4tqlGcAH5Xq2wo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ra4tqlGcAH5Xq2wo .error-icon{fill:#552222;}#mermaid-svg-ra4tqlGcAH5Xq2wo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ra4tqlGcAH5Xq2wo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .marker.cross{stroke:#333333;}#mermaid-svg-ra4tqlGcAH5Xq2wo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ra4tqlGcAH5Xq2wo p{margin:0;}#mermaid-svg-ra4tqlGcAH5Xq2wo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster-label text{fill:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster-label span{color:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster-label span p{background-color:transparent;}#mermaid-svg-ra4tqlGcAH5Xq2wo .label text,#mermaid-svg-ra4tqlGcAH5Xq2wo span{fill:#333;color:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .node rect,#mermaid-svg-ra4tqlGcAH5Xq2wo .node circle,#mermaid-svg-ra4tqlGcAH5Xq2wo .node ellipse,#mermaid-svg-ra4tqlGcAH5Xq2wo .node polygon,#mermaid-svg-ra4tqlGcAH5Xq2wo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .rough-node .label text,#mermaid-svg-ra4tqlGcAH5Xq2wo .node .label text,#mermaid-svg-ra4tqlGcAH5Xq2wo .image-shape .label,#mermaid-svg-ra4tqlGcAH5Xq2wo .icon-shape .label{text-anchor:middle;}#mermaid-svg-ra4tqlGcAH5Xq2wo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .rough-node .label,#mermaid-svg-ra4tqlGcAH5Xq2wo .node .label,#mermaid-svg-ra4tqlGcAH5Xq2wo .image-shape .label,#mermaid-svg-ra4tqlGcAH5Xq2wo .icon-shape .label{text-align:center;}#mermaid-svg-ra4tqlGcAH5Xq2wo .node.clickable{cursor:pointer;}#mermaid-svg-ra4tqlGcAH5Xq2wo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .arrowheadPath{fill:#333333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ra4tqlGcAH5Xq2wo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ra4tqlGcAH5Xq2wo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ra4tqlGcAH5Xq2wo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster text{fill:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo .cluster span{color:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ra4tqlGcAH5Xq2wo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ra4tqlGcAH5Xq2wo rect.text{fill:none;stroke-width:0;}#mermaid-svg-ra4tqlGcAH5Xq2wo .icon-shape,#mermaid-svg-ra4tqlGcAH5Xq2wo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ra4tqlGcAH5Xq2wo .icon-shape p,#mermaid-svg-ra4tqlGcAH5Xq2wo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ra4tqlGcAH5Xq2wo .icon-shape .label rect,#mermaid-svg-ra4tqlGcAH5Xq2wo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ra4tqlGcAH5Xq2wo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ra4tqlGcAH5Xq2wo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ra4tqlGcAH5Xq2wo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
发起 POST /login
UsernamePasswordAuthenticationFilter
request.getParameter 获取用户名密码
构造 UsernamePasswordAuthenticationToken
设置 Request Details
调用 AuthenticationManager.authenticate
认证成功?
调用 SuccessHandler
调用 FailureHandler

这段逻辑给了我们明确的启示:我们只需要 替换凭证提取方式 ,将 Parameter 替换为 RequestBody 的 JSON 解析,其余逻辑(如认证委托、详情设置)完全可以复用。

自定义 RestAuthenticationFilter

接下来,我们编写自定义过滤器 RestAuthenticationFilter,使其继承自 AbstractAuthenticationProcessingFilter 或直接模仿 UsernamePasswordAuthenticationFilter 的结构。为了更清晰地展示原理,这里我们继承 UsernamePasswordAuthenticationFilter 并重写核心方法。

第一步:创建过滤器类

我们需要引入 ObjectMapper 用于解析 JSON,并重写 attemptAuthentication 方法。

java 复制代码
package com.example.demo.filter;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
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.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final ObjectMapper objectMapper;

    public RestAuthenticationFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        // 默认登录路径,可根据需要修改
        super.setFilterProcessesUrl("/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 1. 校验请求方法必须为 POST
        if (!"POST".equalsIgnoreCase(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        // 2. 从 Request Body 中读取 JSON 并解析用户名密码
        try {
            BufferedReader reader = request.getReader();
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            JsonNode jsonNode = objectMapper.readTree(sb.toString());

            String username = jsonNode.has("username") ? jsonNode.get("username").asText() : "";
            String password = jsonNode.has("password") ? jsonNode.get("password").asText() : "";

            // 移除首尾空格
            if (username != null) {
                username = username.trim();
            }

            // 3. 构造 Authentication 对象(未认证状态)
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);

            // 4. 设置详情(IP、Session 等)
            setDetails(request, authRequest);

            // 5. 委托 AuthenticationManager 进行认证
            return this.getAuthenticationManager().authenticate(authRequest);

        } catch (IOException e) {
            throw new AuthenticationServiceException("Failed to parse JSON request body", e);
        }
    }
}

配置 Json 响应处理器

为了保持前后端分离的友好性,我们需要将默认的页面重定向处理器替换为返回 JSON 格式的处理器。

java 复制代码
package com.example.demo.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;

    public JsonAuthenticationSuccessHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "Login successful");
        result.put("username", authentication.getName());
        // 这里可以根据需要添加更多信息,如 JWT Token 等
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}
java 复制代码
package com.example.demo.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final ObjectMapper objectMapper;

    public JsonAuthenticationFailureHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", exception.getMessage());
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

整合至 SecurityFilterChain

有了自定义 Filter 和处理器,下一步是将它们组装到 Spring Security 的配置中。这里的关键点是 替换 默认的 UsernamePasswordAuthenticationFilter,因为我们要完全接管登录逻辑。

SecurityConfig 中:

java 复制代码
package com.example.demo.config;

import com.example.demo.filter.RestAuthenticationFilter;
import com.example.demo.handler.JsonAuthenticationFailureHandler;
import com.example.demo.handler.JsonAuthenticationSuccessHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ObjectMapper objectMapper;

    public SecurityConfig(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    // 1. 定义用户数据源(内存版示例)
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan")
                .password("{noop}12345678")
                .roles("USER")
                .build());
        return manager;
    }

    // 2. 注册自定义 Filter Bean
    @Bean
    public RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        // 设置认证成功/失败处理器
        filter.setAuthenticationSuccessHandler(new JsonAuthenticationSuccessHandler(objectMapper));
        filter.setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler(objectMapper));
        // 设置 AuthenticationManager(必须)
        filter.setAuthenticationManager(authenticationManagerBean());
        // 设置登录处理路径(可与构造函数中的默认值一致,也可覆盖)
        filter.setFilterProcessesUrl("/auth/login");
        return filter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 CSRF(对于无状态 API 通常建议禁用,或者针对特定路径忽略)
        http.csrf().disable();

        // 请求授权规则
        http.authorizeRequests()
                .antMatchers("/auth/login").permitAll() // 登录入口放行
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/api/**").hasRole("USER")
                .anyRequest().authenticated();

        // 核心:将默认的 UsernamePasswordAuthenticationFilter 替换为我们自定义的 Filter
        http.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

配置演进:处理 CSRF 与路径问题

在测试过程中,可能会遇到 403Invalid CSRF Token 异常。在前后端分离且无状态的场景下,我们通常有两种处理方案:

  1. 全局禁用 CSRF :如上述代码中的 http.csrf().disable()
  2. 针对路径忽略:如果不想全局禁用,可以仅忽略特定路径。

示例

java 复制代码
http.csrf().ignoringAntMatchers("/auth/login");

选择哪种方式取决于项目需求。对于纯粹的 Restful API,推荐全局禁用。

流程可视化

下图展示了自定义 Filter 生效后的完整认证流程:
FailureHandler SuccessHandler UserDetailsService AuthenticationManager RestAuthenticationFilter Client FailureHandler SuccessHandler UserDetailsService AuthenticationManager RestAuthenticationFilter Client #mermaid-svg-Mxh8u3zwsQJk5jlE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Mxh8u3zwsQJk5jlE .error-icon{fill:#552222;}#mermaid-svg-Mxh8u3zwsQJk5jlE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Mxh8u3zwsQJk5jlE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Mxh8u3zwsQJk5jlE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Mxh8u3zwsQJk5jlE .marker.cross{stroke:#333333;}#mermaid-svg-Mxh8u3zwsQJk5jlE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Mxh8u3zwsQJk5jlE p{margin:0;}#mermaid-svg-Mxh8u3zwsQJk5jlE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Mxh8u3zwsQJk5jlE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Mxh8u3zwsQJk5jlE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Mxh8u3zwsQJk5jlE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Mxh8u3zwsQJk5jlE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Mxh8u3zwsQJk5jlE .sequenceNumber{fill:white;}#mermaid-svg-Mxh8u3zwsQJk5jlE #sequencenumber{fill:#333;}#mermaid-svg-Mxh8u3zwsQJk5jlE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Mxh8u3zwsQJk5jlE .messageText{fill:#333;stroke:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Mxh8u3zwsQJk5jlE .labelText,#mermaid-svg-Mxh8u3zwsQJk5jlE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .loopText,#mermaid-svg-Mxh8u3zwsQJk5jlE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Mxh8u3zwsQJk5jlE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Mxh8u3zwsQJk5jlE .noteText,#mermaid-svg-Mxh8u3zwsQJk5jlE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Mxh8u3zwsQJk5jlE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Mxh8u3zwsQJk5jlE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Mxh8u3zwsQJk5jlE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Mxh8u3zwsQJk5jlE .actorPopupMenu{position:absolute;}#mermaid-svg-Mxh8u3zwsQJk5jlE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Mxh8u3zwsQJk5jlE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Mxh8u3zwsQJk5jlE .actor-man circle,#mermaid-svg-Mxh8u3zwsQJk5jlE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Mxh8u3zwsQJk5jlE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 认证成功 认证失败 POST /auth/login (JSON) 解析 JSON ->> 提取 username/password 调用 authenticate(token) 加载用户信息 返回 UserDetails 密码比对/校验 返回 Authentication 调用 onAuthenticationSuccess 返回 200 JSON 抛出 AuthenticationException 调用 onAuthenticationFailure 返回 401 JSON

测试验证

使用 Postman 或任何 HTTP 客户端发送请求:

  • URL : POST http://localhost:8080/auth/login
  • Headers : Content-Type: application/json
  • Body:
json 复制代码
{
    "username": "zhangsan",
    "password": "12345678"
}

预期响应(成功)

json 复制代码
{
    "code": 200,
    "message": "Login successful",
    "username": "zhangsan"
}

预期响应(失败,密码错误)

json 复制代码
{
    "code": 401,
    "message": "Bad credentials"
}

关键 API 速览

  • AbstractAuthenticationProcessingFilter:自定义认证 Filter 的基类,定义 attemptAuthentication 抽象方法。
  • AuthenticationManager:认证管理器,负责协调认证流程。
  • UsernamePasswordAuthenticationToken:用户名密码凭证的载体。
  • SecurityFilterChain:过滤链配置,addFilterAt 用于在指定位置插入过滤器。
  • AuthenticationSuccessHandler / AuthenticationFailureHandler:用于自定义认证结果响应。

官方文档

总结

通过本文的分析与实战,我们从 UsernamePasswordAuthenticationFilter 的源码出发,成功构建了一个支持 JSON 请求体的认证过滤器。

我们不仅替换了默认的凭证提取机制,还集成了自定义的 JSON 响应处理器,从而完美适配了前后端分离架构。该方案保留了 Spring Security 原有的认证管理能力,仅在交互入口处进行了扩展,做到了低侵入、高复用。

相关推荐
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之密码验证规则与自定义注解
java·数据库·spring
一嘴一个橘子2 小时前
java - redis 缓存击穿 - 互斥锁
java
shehuiyuelaiyuehao2 小时前
算法39,位运算,消失的两个数字
java·数据结构·算法
小刘在重生~2 小时前
Java 异常体系完整笔记
java·笔记·python
抠脚小弟2 小时前
Spring Task 定时任务详解:从入门到实战
java·后端·spring
2601_962284502 小时前
Python 和Java 哪个更适合做自动化测试?
java·自动化测试·python·接口测试·性能测试
随遇而安zx2 小时前
【多线程】---AQS 原理 知识点(设计思想与源码深度解析)
java·多线程·aqs
LXMXHJ2 小时前
springboot中的线程操作
java·spring boot·后端·线程
淼澄研学2 小时前
Win11 Dev Home与WinGet实操:基于JSON的声明式环境配置指南
json