概述
在前后端分离的开发模式下,传统的表单提交(Form Login)已经无法满足无状态、JSON交互的需求。Spring Security 默认的 UsernamePasswordAuthenticationFilter 从 HttpServletRequest 中提取 parameter 来获取用户名和密码,这显然不适用于 application/json 类型的请求体。
本文将深入分析 UsernamePasswordAuthenticationFilter 的源码逻辑,并手把手带你实现一个自定义的 RestAuthenticationFilter,使其支持从 JSON 中读取凭证。同时,我们将讨论如何将这个过滤器注册到 Spring Security 的过滤器链中,并处理 CSRF 防护带来的问题,最终实现一个纯 JSON 交互的认证端点。
核心概念与代码结构
-
核心概念 :
Filter生命周期、AuthenticationManager、Authentication对象、ObjectMapper序列化、SecurityFilterChain配置。 -
涉及组件:
UsernamePasswordAuthenticationFilter(默认表单过滤器)RestAuthenticationFilter(自定义 JSON 过滤器)AuthenticationSuccessHandler与AuthenticationFailureHandler(自定义响应处理器)HttpSecurity配置与addFilterAt/addFilterBefore注册方式。
-
项目结构预览:
dirsrc/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 方法中,核心流程如下:
- 请求方法校验 :若非
POST请求,直接抛出异常。 - 提取凭证 :通过
request.getParameter(usernameParameter)和request.getParameter(passwordParameter)从表单参数中获取用户名和密码。 - 封装 Token :构造
UsernamePasswordAuthenticationToken对象。 - 设置详情 :调用
setDetails(request, token)存入 IP、Session 等信息。 - 认证委托 :调用
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 与路径问题
在测试过程中,可能会遇到 403 或 Invalid CSRF Token 异常。在前后端分离且无状态的场景下,我们通常有两种处理方案:
- 全局禁用 CSRF :如上述代码中的
http.csrf().disable()。 - 针对路径忽略:如果不想全局禁用,可以仅忽略特定路径。
示例
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:用于自定义认证结果响应。
官方文档
- Spring Security Reference: Servlet Authentication
- Jackson ObjectMapper: Jackson Databind
总结
通过本文的分析与实战,我们从 UsernamePasswordAuthenticationFilter 的源码出发,成功构建了一个支持 JSON 请求体的认证过滤器。
我们不仅替换了默认的凭证提取机制,还集成了自定义的 JSON 响应处理器,从而完美适配了前后端分离架构。该方案保留了 Spring Security 原有的认证管理能力,仅在交互入口处进行了扩展,做到了低侵入、高复用。