Spring Security 实战:手把手实现 Email + Password 自定义认证
一、为什么要写这篇文章?
在企业项目中,使用邮箱或手机号作为登录方式是非常常见的需求,而 Spring Security 默认只支持基于用户名(username)的认证流程,难以直接满足灵活的业务场景。
本文将以“邮箱 + 密码”登录为例,带你从零实现一个完整的 Spring Security 自定义认证流程,涵盖以下核心能力:
-
自定义登录接口(支持 JSON 请求体)
-
自定义认证令牌(AuthenticationToken)
-
自定义认证逻辑(AuthenticationProvider)
-
自定义过滤器(AuthenticationFilter)
-
注册配置到 Spring Security 体系中
该项目基于:
-
JDK 17
-
Spring Boot 2.7.18
-
Spring Security 5.7.x
完整示例项目地址:
二、项目结构与模块说明
spring-security-basic/
├── config/ ← Security 配置类 + 认证处理器
├── controller/ ← 测试接口,验证权限生效
├── filter/ ← 自定义认证过滤器(接收 email + password)
├── provider/ ← 自定义认证逻辑 Provider
├── token/ ← 自定义 AuthenticationToken
├── exception/ ← 自定义异常(如认证失败)
└── Application.java
三、核心实现拆解
1. 自定义认证 Token
我们自定义一个 EmailPasswordAuthenticationToken 来承载邮箱 + 密码:
public class EmailPasswordAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public EmailPasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public EmailPasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
// must use super, as we override
super.setAuthenticated(true);
}
public static EmailPasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new EmailPasswordAuthenticationToken(principal, credentials);
}
public static EmailPasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new EmailPasswordAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
2. 自定义认证 Provider
用于验证邮箱密码是否正确,并返回认证成功后的 Authentication 对象:
@Component
public class EmailPasswordAuthenticationProvider implements AuthenticationProvider {
private boolean forcePrincipalAsString = false;
private GrantedAuthoritiesMapper grantedAuthoritiesMapper = new NullAuthoritiesMapper();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(EmailPasswordAuthenticationToken.class, authentication,
"EmailPasswordAuthenticationProvider.onlySupports Only EmailPasswordAuthenticationToken is supported");
// 获取token中的用户信息
EmailPasswordAuthenticationToken token = (EmailPasswordAuthenticationToken) authentication;
String email = token.getPrincipal().toString();
String password = token.getCredentials().toString();
// TODO:这里应接入真实的用户服务
UserDetails userDetails = loadUserByEmail(email);
// 验证邮箱密码
boolean equals = userDetails.getPassword().equals(password);
if (!equals) {
throw new EmailPasswordAuthenticationException("邮箱或者密码错误");
}
// 创建成功的Authentication对象
Object principalToReturn = userDetails;
if (this.forcePrincipalAsString) {
principalToReturn = principalToReturn.toString();
}
return createSuccessAuthentication(principalToReturn, authentication, userDetails);
}
private UserDetails loadUserByEmail(String email) {
return new User("testUserName", "tHXEilIZtd",
true, true, true, true,
this.grantedAuthoritiesMapper.mapAuthorities(List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));
}
@Override
public boolean supports(Class<?> authentication) {
return EmailPasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
EmailPasswordAuthenticationToken result = new EmailPasswordAuthenticationToken(principal, authentication.getCredentials(),
this.grantedAuthoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
}
3. 自定义认证 Filter
用于解析请求体中的 email + password 并触发认证:
public class EmailPasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_EMAIL_KEY = "email";
public static final String SPRING_SECURITY_FORM_VERIFICATION_CODE_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/v1/auth/loginByEmailPassword", "POST");
private String emailParameter = SPRING_SECURITY_FORM_EMAIL_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_VERIFICATION_CODE_KEY;
private boolean postOnly = true;
public EmailPasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public EmailPasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String email = obtainUsername(request);
email = (email != null) ? email.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
EmailPasswordAuthenticationToken authRequest = EmailPasswordAuthenticationToken.unauthenticated(email, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.emailParameter);
}
protected void setDetails(HttpServletRequest request, EmailPasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String emailParameter) {
Assert.hasText(emailParameter, "Username parameter must not be empty or null");
this.emailParameter = emailParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.emailParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
四、Spring Security 配置
将自定义组件整合进 Spring Security:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
http.authenticationManager(authenticationManager)
.authorizeRequests()
// 允许所有人访问的接口
.antMatchers("/api/v1/public/**").permitAll()
// 不拦截认证接口
.antMatchers("/api/v1/auth/**").permitAll()
// 拥有指定角色才能访问
.antMatchers("/api/v1/admin/**").hasRole("ADMIN")
.antMatchers("/api/v1/tester/**").hasRole("TESTER")
// 需要登录才能访问
.antMatchers("/api/v1/**").authenticated()
.and()
// csrf配置
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
// 添加自定义的logout
.logout()
.logoutUrl("/api/v1/auth/logout")
.logoutSuccessHandler(new JsonLogoutSuccessHandler())
.and()
// 自定义异常配置
.exceptionHandling()
.accessDeniedHandler(new JsonErrorMsgAccessDeniedHandler())
.authenticationEntryPoint(new JsonErrorMsgAuthenticationEntryPoint())
.and()
.apply(new EmailPasswordAuthenticationConfig<>("/api/v1/auth/loginByEmailPassword"));
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(EmailPasswordAuthenticationProvider emailPasswordProvider) {
return new ProviderManager(List.of(emailPasswordProvider));
}
}
五、测试接口验证
访问localhost:8080/doc.html
编辑
登录接口:
email:随意
password: tHXEilIZtd
返回登录成功信息或错误提示。
验证接口
/api/v1/test → 需要登录才能访问的接口
/api/v1/public/test → 允许所有人访问的接口
/api/v1/admin/test → 有ADMIN角色的用户才能访问的接口
/api/v1/admin/test → 有TESTER角色的用户才能访问的接口
可通过 swagger ui页面 测试接口鉴权是否生效。
六、总结与拓展
本篇文章完整实现了 Spring Security 下基于 Email + Password 的自定义认证流程。
后续你可以基于本项目扩展:
-
支持短信验证码登录(可自定义新的 Token + Provider)
-
登录成功后签发 JWT 令牌
-
接入数据库进行真实用户验证
-
动态权限点管理体系(juejin.cn/post/751975…)
欢迎收藏、点赞、关注后续实战系列文章!