Spring?Security自定義AuthenticationManager實(shí)現(xiàn)手機(jī)號(hào)/密碼雙認(rèn)證
01整體思路 3 步走
- 1. 自定義認(rèn)證提供者
CustomAuthenticationProvider:
識(shí)別登錄方式,分發(fā)給對(duì)應(yīng)UserDetailsService。 - 2. 雙 Service:
•UserDetailsService驗(yàn)證賬號(hào)密碼
•PhoneNumberUserService驗(yàn)證手機(jī)號(hào)驗(yàn)證碼 - 3. 配置注入:把自定義提供者塞進(jìn) Spring Security,讓它乖乖聽(tīng)話。
02自定義認(rèn)證提供者
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService; // 賬號(hào)密碼驗(yàn)證
private final PasswordEncoder passwordEncoder; // 密碼加密器
private final PhoneNumberUserService phoneNumberUserService; // 手機(jī)號(hào)驗(yàn)證
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(); // 密碼或驗(yàn)證碼
UserDetails userDetails;
if (principal.startsWith("username:")) { // 賬號(hào)密碼登錄
String username = principal.substring("username:".length());
userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(credentials, userDetails.getPassword())) {
throw new BadCredentialsException("密碼錯(cuò)誤");
}
} else if (principal.startsWith("phone:")) { // 手機(jī)號(hào)登錄
String phoneNumber = principal.substring("phone:".length());
userDetails = phoneNumberUserService.loadUserByPhoneNumber(phoneNumber); // 這里驗(yàn)證碼校驗(yàn)可放在 service 內(nèi),也可前置過(guò)濾器
else{
throw new BadCredentialsException("登錄方式不支持");
}
// 生成已認(rèn)證令牌
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. 前綴識(shí)別:用
username:和phone:做路由,避免寫(xiě)兩套接口。 - 2. 職責(zé)分離:驗(yàn)證碼校驗(yàn)交給
PhoneNumberUserService,保持單一職責(zé)。 - 3. 線程安全:所有依賴通過(guò)構(gòu)造器注入,無(wú)共享可變狀態(tài),天然并發(fā)友好。
03雙 Service 實(shí)現(xiàn)
UserDetailsService(賬號(hào)密碼版)
@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(手機(jī)號(hào)驗(yàn)證碼版)
@Service
@RequiredArgsConstructor
public class PhoneNumberUserService {
private final UserMapper userMapper;
private final MenuMapper menuMapper;
private final RedisTemplate<String, String> redisTemplate; // 緩存驗(yàn)證碼
public UserDetails loadUserByPhoneNumber(String phoneNumber) { // 1? 查庫(kù)
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhonenumber, phoneNumber));
if (user == null)
throw new RuntimeException("手機(jī)號(hào)未注冊(cè)"); // 2? 查權(quán)限
List<String> perms = menuMapper.selectPermsByUserId(user.getId());
perms.add(user.getRoles()); // 3?驗(yàn)證碼校驗(yàn)示例(可前置過(guò)濾器)
//String codeInRedis = redisTemplate.opsForValue().get("SMS:" + phoneNumber);
return new LoginUser(user, perms);
}
}注解:
- 1. LambdaQueryWrapper:MyBatis-Plus 寫(xiě)法,鏈?zhǔn)角逅?/li>
- 2. 角色權(quán)限合并:把角色當(dāng)權(quán)限塞到同一集合,后續(xù)授權(quán)更絲滑。
- 3. 驗(yàn)證碼解耦:校驗(yàn)邏輯可放在 Service,也可前置過(guò)濾器,靈活插拔。
04SecurityConfig:把自定義提供者塞進(jìn)去
@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 {
// 替換默認(rèn) AuthenticationManager
return new ProviderManager(customAuthenticationProvider());
}
}注解:
- 1. ProviderManager:Spring Security 的核心調(diào)度器,塞入我們的 Provider 就能接管認(rèn)證。
- 2. 構(gòu)造器注入:Spring 推薦寫(xiě)法,避免循環(huán)依賴。
- 3. 無(wú) @Autowired:全部顯式 Bean,方便單測(cè) 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 統(tǒng)一:前端傳
loginType=1賬號(hào)密碼,2手機(jī)號(hào)驗(yàn)證碼,后端零 if-else。 - 2. JWT + Redis:無(wú)狀態(tài) Token + 在線用戶信息緩存,分布式登錄穩(wěn)穩(wěn)的。
- 3. 異常透?jìng)?/strong>:認(rèn)證失敗直接拋異常,被全局異常處理器統(tǒng)一包裝,前端拿到統(tǒng)一格式。
測(cè)試
| 登錄方式 | 請(qǐng)求體 | 返回 |
|---|---|---|
| 賬號(hào)密碼 | {"loginType":1,"username":"yuqn","credential":"123456"} | {"msg":"登錄成功","token":"eyJ..."} |
| 手機(jī)驗(yàn)證碼 | {"loginType":2,"phone":"13800138000","credential":"8888"} | 同上 |
到此這篇關(guān)于Spring Security自定義AuthenticationManager實(shí)現(xiàn)手機(jī)號(hào)/密碼雙認(rèn)證的文章就介紹到這了,更多相關(guān)Spring Security自定義AuthenticationManager內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis使用雙層<foreach>循環(huán)嵌套方式
在Mybatis中使用雙層循環(huán)嵌套插入數(shù)據(jù)可以有效減少數(shù)據(jù)庫(kù)IO操作,提高性能,通過(guò)在mapper.xml中定義雙層集合來(lái)實(shí)現(xiàn),外層集合為實(shí)體類中的集合字段,內(nèi)層集合為集合字段中的另一個(gè)集合,通過(guò)這種方式,可以在業(yè)務(wù)代碼中減少循環(huán)插入的次數(shù)2024-09-09
Mybatis流式查詢并實(shí)現(xiàn)將結(jié)果分批寫(xiě)入文件
這篇文章主要介紹了Mybatis流式查詢并實(shí)現(xiàn)將結(jié)果分批寫(xiě)入文件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
springboot引用kettle實(shí)現(xiàn)對(duì)接oracle數(shù)據(jù)的示例代碼
這篇文章主要介紹了springboot引用kettle實(shí)現(xiàn)對(duì)接oracle數(shù)據(jù),其實(shí)kettle集成到springboot里面沒(méi)有多少代碼,這個(gè)功能最主要的還是ktr文件的編寫(xiě),只要ktr編寫(xiě)好了,放到指定文件夾下,寫(xiě)個(gè)定時(shí)任務(wù)就完事了,需要的朋友可以參考下2022-12-12
springboot常見(jiàn)的一些相關(guān)注解總結(jié)
在springboot中,摒棄了spring以往項(xiàng)目中大量繁瑣的配置,遵循約定大于配置的原則,通過(guò)自身默認(rèn)配置,極大的降低了項(xiàng)目搭建的復(fù)雜度,這篇文章主要介紹了springboot常見(jiàn)的一些相關(guān)注解,需要的朋友可以參考下2025-10-10
Maven中Junit測(cè)試@Test等注解無(wú)法識(shí)別的問(wèn)題及解決
這篇文章主要介紹了Maven中Junit測(cè)試@Test等注解無(wú)法識(shí)別的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
java并發(fā)編程專題(九)----(JUC)淺析CyclicBarrier
這篇文章主要介紹了java CyclicBarrier的相關(guān)資料,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07

