spring security设置多个数据源和登录验证码
在 Spring Security 中集成多数据源和验证码登录需要结合自定义认证逻辑和过滤器链扩展。以下是完整实现方案及代码示例:
一、多数据源认证配置
1. 定义两个数据源的 UserDetailsService
// 数据源1:数据库用户
@Service("db1UserDetailsService")
public class DB1UserDetailsService implements UserDetailsService {
@Autowired
private UserRepositoryDB1 userRepoDB1;
@Override
public UserDetails loadUserByUsername(String username) {
UserDB1 user = userRepoDB1.findByUsername(username);
return new CustomUserDetails(user.getUsername(), user.getPassword(), "DB1_ROLE");
}
}
// 数据源2:外部API用户
@Service("db2UserDetailsService")
public class DB2UserDetailsService implements UserDetailsService {
@Autowired
private ApiUserClient apiUserClient;
@Override
public UserDetails loadUserByUsername(String username) {
UserApi user = apiUserClient.fetchUser(username);
return new CustomUserDetails(user.getUsername(), user.getPassword(), "DB2_ROLE");
}
}
2. 自定义 AuthenticationProvider
选择数据源
public class MultiSourceAuthProvider implements AuthenticationProvider {
@Autowired
@Qualifier("db1UserDetailsService")
private UserDetailsService db1Service;
@Autowired
@Qualifier("db2UserDetailsService")
private UserDetailsService db2Service;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
String username = auth.getName();
// 根据用户名前缀选择数据源(如 "db1:user")
UserDetails userDetails = null;
if (username.startsWith("db1:")) {
userDetails = db1Service.loadUserByUsername(username.substring(4));
} else if (username.startsWith("db2:")) {
userDetails = db2Service.loadUserByUsername(username.substring(4));
} else {
throw new BadCredentialsException("Invalid username prefix");
}
// 验证密码
if (passwordEncoder.matches((String) auth.getCredentials(), userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
} else {
throw new BadCredentialsException("Password mismatch");
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
3. 注册 AuthenticationProvider
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
);
return http.build();
}
@Bean
public AuthenticationProvider multiSourceAuthProvider() {
return new MultiSourceAuthProvider();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
二、验证码集成
1. 生成验证码(使用 Kaptcha 库)
<!-- pom.xml 添加依赖 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
@Configuration
public class CaptchaConfig {
@Bean
public Producer captchaProducer() {
Properties props = new Properties();
props.setProperty("kaptcha.textproducer.char.length", "4");
props.setProperty("kaptcha.image.width", "120");
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(new Config(props));
return kaptcha;
}
}
2. 验证码接口与缓存
@RestController
public class CaptchaController {
@Autowired
private Producer captchaProducer;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/captcha")
public void generateCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
String code = captchaProducer.createText();
String sessionId = request.getSession().getId();
// 存储到 Redis(过期时间 2 分钟)
redisTemplate.opsForValue().set("captcha:" + sessionId, code, 2, TimeUnit.MINUTES);
response.setContentType("image/jpeg");
BufferedImage image = captchaProducer.createImage(code);
ImageIO.write(image, "jpg", response.getOutputStream());
}
}
3. 自定义验证码过滤器
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if ("/login".equals(request.getRequestURI()) && "POST".equalsIgnoreCase(request.getMethod())) {
String sessionId = request.getSession().getId();
String captchaCode = redisTemplate.opsForValue().get("captcha:" + sessionId);
String userInput = request.getParameter("captcha");
if (userInput == null || !userInput.equalsIgnoreCase(captchaCode)) {
throw new AuthenticationServiceException("验证码错误");
}
}
chain.doFilter(request, response);
}
}
4. 将过滤器插入到 Spring Security 链中
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new CaptchaFilter(), UsernamePasswordAuthenticationFilter.class) // 插入验证码过滤器
// 其他配置...
return http.build();
}
}
三、前端集成示例
<!-- login.html -->
<form action="/login" method="post">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<input type="text" name="captcha" placeholder="验证码">
<img src="/captcha" onclick="this.src='/captcha?t='+new Date().getTime()">
<button>登录</button>
</form>
四、关键配置总结
功能 | 实现方式 |
---|---|
多数据源选择 | 自定义 AuthenticationProvider ,根据用户名前缀选择 UserDetailsService |
验证码生成 | 使用 Kaptcha 生成图片,存储到 Redis |
验证码校验 | 自定义过滤器拦截 /login 请求,比对输入值与 Redis 缓存 |
密码加密 | 统一使用 BCryptPasswordEncoder |
五、扩展优化建议
- 动态数据源路由:通过注解或配置文件动态添加更多数据源。
- 验证码类型:支持短信验证码或邮件验证码。
- 限流防护:对验证码接口添加限流(如使用
Resilience4j
)。 - 分布式 Session:使用 Spring Session + Redis 解决集群环境 Session 共享问题。
通过以上方案,可灵活扩展 Spring Security 的认证能力,满足复杂业务场景需求。