Appearance
Spring Security 自定义认证流程
1. 核心组件
在 Spring Security 框架中,AuthenticationManager、AuthenticationProvider 和 UserDetailsService 是认证流程的核心组件,它们各自承担不同的角色,协同完成用户认证的过程。以下是它们的工作原理及协作方式:
1.1. AuthenticationManager
AuthenticationManager 是认证的核心接口,它负责管理和协调认证流程。其主要职责是接收一个 Authentication 对象(包含用户输入的凭证,比如用户名和密码)并尝试验证。如果验证成功,它将返回一个经过认证的 Authentication 对象;如果失败,则抛出一个认证异常。
AuthenticationManager 通常有多个 AuthenticationProvider 支持,并通过委托这些 AuthenticationProvider 完成具体的认证工作。Spring 提供了一个常用的实现类 ProviderManager,该类会循环遍历注册的 AuthenticationProvider,尝试找到一个可以处理该类型认证请求的提供者。
1.2. AuthenticationProvider
AuthenticationProvider 是实际执行认证逻辑的组件接口。它负责根据特定的认证方式(例如用户名密码认证、OAuth认证等)验证用户信息。AuthenticationProvider 的 authenticate() 方法接收一个 Authentication 对象,进行以下步骤:
- 检查当前
AuthenticationProvider是否支持这种Authentication类型(通过supports方法); - 如果支持,则会根据
Authentication中的凭证信息(例如用户名、密码等)来完成验证; - 一旦认证成功,返回一个带有用户信息和权限信息的
Authentication对象; - 如果认证失败,会抛出一个认证异常;
1.3. UserDetailsService
UserDetailsService 是用于加载用户信息的接口。它的主要职责是根据用户名从数据库或其他数据源中加载用户信息,并返回一个 UserDetails 对象(包含用户名、密码、权限等信息)。
UserDetailsService 通常会在 DaoAuthenticationProvider 中使用,当 DaoAuthenticationProvider 收到一个用户名和密码的认证请求时,它会调用 UserDetailsService 的 loadUserByUsername() 方法,获取 UserDetails,然后对用户输入的密码进行验证。
1.4. UserDetails
在 Spring Security 框架中,UserDetails 对象并不是绝对必须的,但它是一个推荐的标准接口,用于封装用户的详细信息。在 Spring Security 默认的用户名和密码标准认证流程中,UserDetails 是默认的用户信息载体。因此,如果你采用的是 Spring Security 提供的 DaoAuthenticationProvider 进行认证,UserDetails 对象就是必须的,因为该提供者会依赖 UserDetails 中的用户信息来验证用户身份和权限。
如果不想使用 UserDetails,可以选择在自定义 AuthenticationProvider 中定义用户信息载体。实现自定义的 Authentication 和 AuthenticationProvider,以其他方式存储和验证用户信息。
1.5. AuthenticationEntryPoint
当未认证的用户尝试访问受保护的资源时,Spring Security 会通过异常处理机制抛出 AuthenticationException。此时,ExceptionTranslationFilter 会捕获异常并调用配置的 AuthenticationEntryPoint 处理未认证请求。AuthenticationEntryPoint 决定了如何响应未认证用户的访问请求:
引导用户至登录页面:在 Web 应用中,
AuthenticationEntryPoint通常会将用户重定向到登录页面,引导用户进行登录。例如,表单登录时,Spring Security 会使用LoginUrlAuthenticationEntryPoint将未登录用户重定向到登录页面。返回 HTTP 错误状态:在 RESTful API 中,通常不会重定向用户,而是返回 401 Unauthorized 状态代码来表示用户未认证。这种情况下可以自定义
AuthenticationEntryPoint返回 JSON 错误消息或自定义响应内容。启动其他认证机制:在某些认证方式(如 Basic Auth 或 OAuth2)中,
AuthenticationEntryPoint可以触发特定的认证流程。例如,在 Basic Auth 中,Spring Security 使用BasicAuthenticationEntryPoint,返回WWW-Authenticate响应头来提示客户端提供凭据。
Spring Security 提供了一些内置的 AuthenticationEntryPoint 实现,常见的包括:
LoginUrlAuthenticationEntryPoint:用于表单登录场景,会将用户重定向到指定的登录 URL。默认情况下,Spring Security 会将用户重定向到/login。BasicAuthenticationEntryPoint:用于 HTTP Basic 认证场景,会向客户端返回WWW-Authenticate头,提示客户端提供用户名和密码。HttpStatusEntryPoint:可以用于返回特定的 HTTP 状态码,例如 401(Unauthorized)或 403(Forbidden),通常用于 REST API 返回统一的未认证响应。
1.6. 认证过程
Spring Security 中的认证过程大致如下:
用户提交登录请求(例如,用户名和密码)。
AuthenticationManager接收到包含用户凭证的Authentication对象。AuthenticationManager遍历所有配置的AuthenticationProvider,找到支持该Authentication类型的提供者。支持该认证请求的
AuthenticationProvider开始处理该Authentication对象。例如,DaoAuthenticationProvider会调用UserDetailsService根据用户名加载UserDetails。UserDetailsService返回包含用户信息的UserDetails对象,DaoAuthenticationProvider会比对用户输入的密码与UserDetails中的密码。如果密码匹配,
DaoAuthenticationProvider会返回一个包含用户信息和权限的Authentication对象给AuthenticationManager,标识认证成功。AuthenticationManager返回认证成功的Authentication对象,如果认证失败,抛出认证异常。
简而言之:
AuthenticationManager是认证的总入口,协调AuthenticationProvider完成认证工作;AuthenticationProvider执行实际的认证逻辑,可以支持不同的认证方式;UserDetailsService提供用户信息的加载功能,通常与数据库交互;
2. 自定义多种认证流程
在 Spring Security 中,可以通过配置不同的 AuthenticationProvider 和自定义认证逻辑来实现多种登录方式。常见的多种登录方式包括:
- 用户名密码登录;
- 手机验证码登录;
- 第三方 OAuth2 登录(如微信、GitHub);
- JWT 令牌认证;
- 其他自定义认证方式;
以用户名密码登录和手机验证码登录为例,演示如何基于 Spring Security 实现多种登录方式。
2.1. 自定义 Authentication
创建不同的 Authentication 实现,用于区分不同的登录方式。例如,可以创建 UsernamePasswordAuthenticationToken 用于用户名密码登录,创建 SmsCodeAuthenticationToken 用于手机验证码登录。
Note:
AbstractAuthenticationToken是Authentication接口的抽象类。
Java
// 用户名密码认证 Token
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
// getters and other overrides...
}
// 手机验证码认证 Token
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private String smsCode;
public SmsCodeAuthenticationToken(Object principal, String smsCode) {
super(null);
this.principal = principal;
this.smsCode = smsCode;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
// getters and other overrides...
}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
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
2.2. 实现各自的 AuthenticationProvider
分别为用户名密码和手机验证码登录实现 AuthenticationProvider。
Java
// 用户名密码认证的 AuthenticationProvider
@Component
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
private final UsernameUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public UsernamePasswordAuthenticationProvider(UsernameUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
} else {
throw new BadCredentialsException("Invalid credentials");
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
// 手机验证码认证的 AuthenticationProvider
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private final PhoneUserDetailsService userDetailsService;
public SmsCodeAuthenticationProvider(PhoneUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phoneNumber = (String) authentication.getPrincipal();
String smsCode = (String) authentication.getCredentials();
// 验证验证码是否正确(通常会调用验证码服务)
if (isValidSmsCode(phoneNumber, smsCode)) {
UserDetails user = userDetailsService.loadUserByUsername(phoneNumber);
return new SmsCodeAuthenticationToken(user, smsCode, user.getAuthorities());
} else {
throw new BadCredentialsException("Invalid SMS code");
}
}
private boolean isValidSmsCode(String phoneNumber, String smsCode) {
// 假设存在某个方法用来验证短信验证码
return true; // 简化示例
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}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
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
2.3. 配置 AuthenticationManager 注册 AuthenticationProvider
通过 Spring Security 配置类,将不同的 AuthenticationProvider 注册到 AuthenticationManager 中。
Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(List<AuthenticationProvider> authenticationProviders) {
return new ProviderManager(authenticationProviders);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin() // 配置表单登录
.and()
.addFilterBefore(new SmsCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 增加自定义过滤器
return http.build();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2.4. 自定义过滤器处理不同的登录请求(可选)
通过自定义过滤器,将不同类型的登录请求分配到对应的 AuthenticationProvider。例如,可以通过 URL 路径或请求参数来区分是用户名密码登录还是手机验证码登录。
Java
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST")); // 只拦截 /login/sms 请求
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String phoneNumber = request.getParameter("phone");
String smsCode = request.getParameter("smsCode");
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phoneNumber, smsCode);
return this.getAuthenticationManager().authenticate(authRequest);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13