扩展登录方式
2025年2月24日大约 4 分钟项目扩展登录方式
提示
以下将以短信验证码登录这个常见场景为例,详细说明如何扩展 Spring Security 的认证机制。
自定义认证 Token
SmsAuthenticationToken
继承自 AbstractAuthenticationToken
,通过策略模式由 AuthenticationManager
根据该 token 类型匹配相应的 Provider 进行认证,其中 Provider 负责具体的验证逻辑。
/**
* 短信验证码认证 Token
*
* @author youlai
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 621L;
/**
* 认证信息 (手机号)
*/
private final Object principal;
/**
* 凭证信息 (短信验证码)
*/
private final Object credentials;
/**
* 短信验证码认证 Token (未认证)
*
* @param principal 微信用户信息
*/
public SmsAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
/**
* 短信验证码认证 Token (已认证)
*
* @param principal 用户信息
* @param authorities 授权信息
*/
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
// 认证通过
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 用户信息
* @param authorities 授权信息
* @return SmsAuthenticationToken
*/
public static SmsAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new SmsAuthenticationToken(principal, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
自定义认证 Provider
Provider 的主要功能是执行验证逻辑。在短信验证码登录中,核心任务是校验验证码是否正确。SmsAuthenticationProvider
专门负责处理短信验证码的校验逻辑,确保登录流程的安全性和准确性。
/**
* 短信验证码认证 Provider
*
* @author youlai
*/
@Slf4j
public class SmsAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
/**
* 短信验证码认证逻辑,参考 Spring Security 认证密码校验流程
*
* @param authentication 认证对象
* @return 认证后的 Authentication 对象
* @throws AuthenticationException 认证异常
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
String inputVerifyCode = (String) authentication.getCredentials();
// 根据手机号获取用户信息
UserAuthInfo userAuthInfo = userService.getUserAuthInfoByMobile(mobile);
if (userAuthInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 校验发送短信验证码的手机号是否与当前登录用户一致
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new BadCredentialsException("验证码错误");
} else {
// 验证成功后删除验证码
redisTemplate.delete(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
}
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
// 创建已认证的 SmsAuthenticationToken
return SmsAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Spring Security 配置
在 SecurityConfig
中注册自定义的 SmsAuthenticationProvider
,实现短信验证码登录认证逻辑:
/**
* Spring Security 配置类
*
* @author youlai
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final RedisTemplate<String, Object> redisTemplate;
private final UserService userService;
// ....
/**
* 默认账号密码认证的 Provider
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
return daoAuthenticationProvider;
}
/**
* 短信验证码认证 Provider
*/
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider(userService, redisTemplate);
}
/**
* 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(
DaoAuthenticationProvider daoAuthenticationProvider,
SmsAuthenticationProvider smsAuthenticationProvider
) {
return new ProviderManager(
daoAuthenticationProvider,
smsAuthenticationProvider
);
}
}
发送验证码
控制层实现
@Operation(summary = "发送登录短信验证码")
@PostMapping("/login/sms/code")
public Result<?> sendLoginVerifyCode(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
) {
authService.sendSmsLoginCode(mobile);
return Result.success();
}
服务层实现
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 发送登录短信验证码
*
* @param mobile 手机号
*/
@Override
public void sendSmsLoginCode(String mobile) {
// 随机生成4位验证码
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码
String code = "1234";
// 发送短信验证码
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
try {
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
} catch (Exception e) {
log.error("发送短信验证码失败", e);
}
// 缓存验证码至Redis,用于登录校验
redisTemplate.opsForValue().set(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile, code, 5, TimeUnit.MINUTES);
}
}
登录接口
控制层实现
/**
* 认证控制层
*
* @author youlai
*/
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
// ...
@Operation(summary = "短信验证码登录")
@PostMapping("/login/sms")
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> loginBySms(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile,
@Parameter(description = "验证码", example = "1234") @RequestParam String code
) {
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
return Result.success(loginResult);
}
}
服务层实现
/**
* 认证服务实现类
*
* @author youlai
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 访问令牌
*/
@Override
public AuthenticationToken loginBySms(String mobile, String code) {
// 1. 创建用户短信验证码认证的令牌(未认证)
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code);
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationToken;
}
}
接口测试
访问接口文档 http://localhost:8989/doc.html,调用发送短信验证码接口,输入手机号。
模拟接收短信验证码(固定为 1234
),调用短信验证码登录接口,输入手机号和验证码即可成功登录并返回访问令牌。