Spring Security自定义AuthenticationManager实现手机号/密码双认证
作者:码语智行
这篇文章给大家介绍了Spring Security自定义AuthenticationManager实现手机号/密码双认证,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧
01整体思路 3 步走
- 1. 自定义认证提供者
CustomAuthenticationProvider:
识别登录方式,分发给对应UserDetailsService。 - 2. 双 Service:
•UserDetailsService验证账号密码
•PhoneNumberUserService验证手机号验证码 - 3. 配置注入:把自定义提供者塞进 Spring Security,让它乖乖听话。
02自定义认证提供者
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService; // 账号密码验证
private final PasswordEncoder passwordEncoder; // 密码加密器
private final PhoneNumberUserService phoneNumberUserService; // 手机号验证
public CustomAuthenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder,
PhoneNumberUserService phoneNumberUserService) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
this.phoneNumberUserService = phoneNumberUserService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String principal = (String) authentication.getPrincipal(); // username:xxx 或 phone:xxx
String credentials = (String) authentication.getCredentials(); // 密码或验证码
UserDetails userDetails;
if (principal.startsWith("username:")) { // 账号密码登录
String username = principal.substring("username:".length());
userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(credentials, userDetails.getPassword())) {
throw new BadCredentialsException("密码错误");
}
} else if (principal.startsWith("phone:")) { // 手机号登录
String phoneNumber = principal.substring("phone:".length());
userDetails = phoneNumberUserService.loadUserByPhoneNumber(phoneNumber); // 这里验证码校验可放在 service 内,也可前置过滤器
else{
throw new BadCredentialsException("登录方式不支持");
}
// 生成已认证令牌
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, credentials, userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override public boolean supports (Class < ? > authentication){
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
}注解:
- 1. 前缀识别:用
username:和phone:做路由,避免写两套接口。 - 2. 职责分离:验证码校验交给
PhoneNumberUserService,保持单一职责。 - 3. 线程安全:所有依赖通过构造器注入,无共享可变状态,天然并发友好。
03双 Service 实现
UserDetailsService(账号密码版)
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));
if (user == null) throw new UsernameNotFoundException("用户不存在");
List<String> perms = menuMapper.selectPermsByUserId(user.getId());
perms.add(user.getRoles()); // 合并角色
return new LoginUser(user, perms);
}
}PhoneNumberUserService(手机号验证码版)
@Service
@RequiredArgsConstructor
public class PhoneNumberUserService {
private final UserMapper userMapper;
private final MenuMapper menuMapper;
private final RedisTemplate<String, String> redisTemplate; // 缓存验证码
public UserDetails loadUserByPhoneNumber(String phoneNumber) { // 1️ 查库
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhonenumber, phoneNumber));
if (user == null)
throw new RuntimeException("手机号未注册"); // 2️ 查权限
List<String> perms = menuMapper.selectPermsByUserId(user.getId());
perms.add(user.getRoles()); // 3️验证码校验示例(可前置过滤器)
//String codeInRedis = redisTemplate.opsForValue().get("SMS:" + phoneNumber);
return new LoginUser(user, perms);
}
}注解:
- 1. LambdaQueryWrapper:MyBatis-Plus 写法,链式清爽。
- 2. 角色权限合并:把角色当权限塞到同一集合,后续授权更丝滑。
- 3. 验证码解耦:校验逻辑可放在 Service,也可前置过滤器,灵活插拔。
04SecurityConfig:把自定义提供者塞进去
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
//密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public PhoneNumberUserService phoneNumberUserService() {
return new PhoneNumberUserService();
}
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider(userDetailsService(), passwordEncoder(), phoneNumberUserService());
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
// 替换默认 AuthenticationManager
return new ProviderManager(customAuthenticationProvider());
}
}注解:
- 1. ProviderManager:Spring Security 的核心调度器,塞入我们的 Provider 就能接管认证。
- 2. 构造器注入:Spring 推荐写法,避免循环依赖。
- 3. 无 @Autowired:全部显式 Bean,方便单测 Mock。
05登录接口:一行代码双通道
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final RedisTemplate<String, Object> redisTemplate;
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
String principal = dto.getLoginType() == 1 ? "username:" + dto.getUsername() : "phone:" + dto.getPhone();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, dto.getCredential());
Authentication authenticate = authenticationManager.authenticate(token);
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());
redisTemplate.opsForValue().set("login:" + loginUser.getUser().getId(), loginUser);
return Result.OK("登录成功", Map.of("token", jwt));
}
}注解:
- 1. DTO 统一:前端传
loginType=1账号密码,2手机号验证码,后端零 if-else。 - 2. JWT + Redis:无状态 Token + 在线用户信息缓存,分布式登录稳稳的。
- 3. 异常透传:认证失败直接抛异常,被全局异常处理器统一包装,前端拿到统一格式。
测试
| 登录方式 | 请求体 | 返回 |
|---|---|---|
| 账号密码 | {"loginType":1,"username":"yuqn","credential":"123456"} | {"msg":"登录成功","token":"eyJ..."} |
| 手机验证码 | {"loginType":2,"phone":"13800138000","credential":"8888"} | 同上 |
到此这篇关于Spring Security自定义AuthenticationManager实现手机号/密码双认证的文章就介绍到这了,更多相关Spring Security自定义AuthenticationManager内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
