引言
在现代企业应用和互联网平台中,用户身份认证的需求日益复杂。传统的单一用户名密码登录方式已经无法满足多样化的业务场景。用户可能需要通过手机号、邮箱、用户名、第三方社交账号等多种方式进行登录,甚至同一个用户可能拥有多个不同类型的账号。这种多账号登录的需求催生了对灵活、可扩展的身份认证架构的迫切需求。
Spring Security 作为 Java 生态中最成熟、最强大的安全框架,提供了丰富的扩展点和灵活的配置选项,能够很好地支持多账号登录的实现。本文将深入探讨如何基于 Spring Security 构建一个支持多账号登录的认证系统,从基础概念到高级实现,从简单配置到复杂场景,全方位解析多账号登录的技术要点和最佳实践。
多账号登录的业务场景分析
在开始技术实现之前,我们需要明确多账号登录的具体业务场景和需求。不同的业务场景对应着不同的技术实现方案。
常见的多账号类型
- 用户名/邮箱/手机号混合登录:用户可以使用注册时的用户名、绑定的邮箱或手机号中的任意一种方式进行登录
- 多租户账号体系:不同租户下的用户可能使用相同的用户名,但属于不同的租户空间
- 第三方账号集成:除了本地账号外,还支持微信、QQ、GitHub 等第三方账号登录
- 企业内部多系统账号:员工可能拥有多个系统的账号,需要统一认证
- 角色分离账号:同一个用户在不同业务场景下使用不同的身份(如管理员账号和个人账号)
技术挑战
实现多账号登录面临的主要技术挑战包括:
- 账号唯一性保证:如何确保不同类型的账号标识符不会冲突
- 认证逻辑复杂化:需要根据不同的登录方式执行不同的认证逻辑
- 用户体验一致性:无论使用哪种方式登录,都应该提供一致的用户体验
- 安全性考虑:不同登录方式的安全级别可能不同,需要相应的安全策略
- 数据模型设计:如何设计数据库表结构来支持多账号体系
Spring Security 核心组件回顾
在深入多账号登录实现之前,让我们先回顾一下 Spring Security 的核心组件,这些组件将在我们的实现中发挥关键作用。
AuthenticationManager 认证管理器
AuthenticationManager 是 Spring Security 认证流程的核心接口。它负责接收 Authentication 对象并返回经过认证的 Authentication 对象。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationProvider 认证提供者
AuthenticationProvider 是具体的认证逻辑实现。AuthenticationManager 通常会委托给一个或多个 AuthenticationProvider 来执行实际的认证工作。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
UserDetailsService 用户详情服务
UserDetailsService 负责根据用户名加载用户信息,返回 UserDetails 对象。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
AuthenticationFilter 认证过滤器
认证过滤器负责从请求中提取认证信息,并创建 Authentication 对象提交给 AuthenticationManager。
基础多账号登录实现方案
最简单的多账号登录实现是在 UserDetailsService 中处理多种账号类型的识别和查询。
数据库设计
首先,我们需要设计支持多账号的数据库表结构:
-- 用户主表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20) UNIQUE,
enabled BOOLEAN DEFAULT TRUE,
account_non_expired BOOLEAN DEFAULT TRUE,
account_non_locked BOOLEAN DEFAULT TRUE,
credentials_non_expired BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 账号类型映射表(可选)
CREATE TABLE user_accounts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIG BIGINT NOT NULL,
account_type VARCHAR(20) NOT NULL, -- 'USERNAME', 'EMAIL', 'PHONE'
account_value VARCHAR(100) NOT NULL,
UNIQUE KEY uk_user_account (user_id, account_type),
UNIQUE KEY uk_account_value (account_type, account_value)
);
自定义 UserDetailsService 实现
@Service
public class MultiAccountUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String loginIdentifier) throws UsernameNotFoundException {
// 判断登录标识符的类型
LoginType loginType = determineLoginType(loginIdentifier);
User user;
switch (loginType) {
case USERNAME:
user = userRepository.findByUsername(loginIdentifier);
break;
case EMAIL:
user = userRepository.findByEmail(loginIdentifier);
break;
case PHONE:
user = userRepository.findByPhone(loginIdentifier);
break;
default:
throw new UsernameNotFoundException("不支持的登录类型: " + loginIdentifier);
}
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + loginIdentifier);
}
return buildUserDetails(user);
}
private LoginType determineLoginType(String identifier) {
if (identifier == null || identifier.trim().isEmpty()) {
return LoginType.UNKNOWN;
}
String trimmed = identifier.trim();
// 判断是否为邮箱
if (trimmed.contains("@") && trimmed.contains(".")) {
return LoginType.EMAIL;
}
// 判断是否为手机号(简单判断,实际项目中需要更严格的验证)
if (trimmed.matches("^1[3-9]\\d{9}$")) {
return LoginType.PHONE;
}
// 默认认为是用户名
return LoginType.USERNAME;
}
private UserDetails buildUserDetails(User user) {
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities("ROLE_USER")
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
enum LoginType {
USERNAME, EMAIL, PHONE, UNKNOWN
}
}
Repository 层实现
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
User findByEmail(String email);
User findByPhone(String phone);
}
Entity 层实现
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true)
private String email;
@Column(unique = true)
private String phone;
private Boolean enabled = true;
private Boolean accountNonExpired = true;
private Boolean accountNonLocked = true;
private Boolean credentialsNonExpired = true;
@CreationTimestamp
private LocalDateTime createdAt;
}
Spring Security 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private MultiAccountUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/register", "/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
这种基础实现方案的优点是简单直接,只需要修改 UserDetailsService 的实现即可。但是它也存在一些局限性:
- 账号类型判断逻辑耦合:账号类型的判断逻辑与业务逻辑耦合在一起
- 扩展性有限:如果需要添加新的登录方式,需要修改现有的判断逻辑
- 性能问题:每次登录都需要进行多次数据库查询来确定账号类型
- 安全性考虑不足:没有针对不同登录方式实施不同的安全策略
基于策略模式的多账号登录实现
为了提高代码的可维护性和扩展性,我们可以使用策略模式来实现多账号登录。
策略接口定义
public interface LoginStrategy {
/**
* 判断当前策略是否支持该登录标识符
*/
boolean supports(String loginIdentifier);
/**
* 根据登录标识符查找用户
*/
User findUser(String loginIdentifier);
/**
* 获取策略的优先级(数值越小优先级越高)
*/
int getPriority();
}
具体策略实现
@Component
@Order(1)
public class UsernameLoginStrategy implements LoginStrategy {
@Autowired
private UserRepository userRepository;
@Override
public boolean supports(String loginIdentifier) {
// 用户名通常不包含特殊字符,且长度在合理范围内
return loginIdentifier != null &&
!loginIdentifier.contains("@") &&
!loginIdentifier.matches("^1[3-9]\\d{9}$") &&
loginIdentifier.length() >= 3 &&
loginIdentifier.length() <= 50;
}
@Override
public User findUser(String loginIdentifier) {
return userRepository.findByUsername(loginIdentifier);
}
@Override
public int getPriority() {
return 1;
}
}
@Component
@Order(2)
public class EmailLoginStrategy implements LoginStrategy {
@Autowired
private UserRepository userRepository;
@Override
public boolean supports(String loginIdentifier) {
if (loginIdentifier == null) {
return false;
}
// 简单的邮箱格式验证
return loginIdentifier.contains("@") &&
loginIdentifier.indexOf('@') > 0 &&
loginIdentifier.lastIndexOf('.') > loginIdentifier.indexOf('@') + 1;
}
@Override
public User findUser(String loginIdentifier) {
return userRepository.findByEmail(loginIdentifier);
}
@Override
public int getPriority() {
return 2;
}
}
@Component
@Order(3)
public class PhoneLoginStrategy implements LoginStrategy {
@Autowired
private UserRepository userRepository;
@Override
public boolean supports(String loginIdentifier) {
if (loginIdentifier == null) {
return false;
}
// 手机号格式验证(中国手机号)
return loginIdentifier.matches("^1[3-9]\\d{9}$");
}
@Override
public User findUser(String loginIdentifier) {
return userRepository.findByPhone(loginIdentifier);
}
@Override
public int getPriority() {
return 3;
}
}
策略上下文管理器
@Service
public class LoginStrategyContext {
private final List<LoginStrategy> strategies;
public LoginStrategyContext(List<LoginStrategy> strategies) {
this.strategies = strategies.stream()
.sorted(Comparator.comparingInt(LoginStrategy::getPriority))
.collect(Collectors.toList());
}
public User findUserByLoginIdentifier(String loginIdentifier) {
for (LoginStrategy strategy : strategies) {
if (strategy.supports(loginIdentifier)) {
User user = strategy.findUser(loginIdentifier);
if (user != null) {
return user;
}
}
}
return null;
}
public boolean canSupportLogin(String loginIdentifier) {
return strategies.stream().anyMatch(strategy -> strategy.supports(loginIdentifier));
}
}
改进的 UserDetailsService
@Service
public class StrategyBasedUserDetailsService implements UserDetailsService {
@Autowired
private LoginStrategyContext loginStrategyContext;
@Override
public UserDetails loadUserByUsername(String loginIdentifier) throws UsernameNotFoundException {
User user = loginStrategyContext.findUserByLoginIdentifier(loginIdentifier);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + loginIdentifier);
}
return buildUserDetails(user);
}
private UserDetails buildUserDetails(User user) {
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities("ROLE_USER")
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
}
这种基于策略模式的实现具有以下优势:
- 高内聚低耦合:每种登录方式的逻辑独立封装
- 易于扩展:添加新的登录方式只需要实现新的策略类
- 灵活性强:可以通过调整优先级来控制策略的执行顺序
- 可测试性好:每个策略都可以独立进行单元测试
多 AuthenticationProvider 方案
对于更复杂的场景,我们可以为每种登录方式创建独立的 AuthenticationProvider。这种方式提供了最大的灵活性,允许为不同的登录方式配置不同的认证逻辑和安全策略。
自定义 Authentication 对象
首先,我们需要为每种登录方式创建对应的 Authentication 对象:
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, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
// 邮箱登录的 Authentication 对象
public class EmailPasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
public EmailPasswordAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public EmailPasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
// 手机号登录的 Authentication 对象
public class PhonePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
public PhonePasswordAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public PhonePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
自定义 AuthenticationProvider
@Component
public class UsernameAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
User user = userRepository.findByUsername(username);
if (user == null) {
throw new BadCredentialsException("用户名或密码错误");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("用户名或密码错误");
}
if (!user.isEnabled()) {
throw new DisabledException("账户已被禁用");
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("账户已过期");
}
if (!user.isAccountNonLocked()) {
throw new LockedException("账户已被锁定");
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("凭证已过期");
}
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_USER");
return new UsernamePasswordAuthenticationToken(username, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
@Component
public class EmailAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
User user = userRepository.findByEmail(email);
if (user == null) {
throw new BadCredentialsException("邮箱或密码错误");
}
// 密码验证和其他检查逻辑同上
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("邮箱或密码错误");
}
// ... 其他验证逻辑
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_USER");
return new EmailPasswordAuthenticationToken(email, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return EmailPasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
@Component
public class PhoneAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
User user = userRepository.findByPhone(phone);
if (user == null) {
throw new BadCredentialsException("手机号或密码错误");
}
// 密码验证和其他检查逻辑同上
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("手机号或密码错误");
}
// ... 其他验证逻辑
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_USER");
return new PhonePasswordAuthenticationToken(phone, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return PhonePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
自定义 AuthenticationFilter
public class MultiAccountAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String SPRING_SECURITY_FORM_LOGIN_TYPE_KEY = "loginType";
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
String loginType = request.getParameter(SPRING_SECURITY_FORM_LOGIN_TYPE_KEY);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
if (loginType == null) {
loginType = "username"; // 默认为用户名登录
}
username = username.trim();
AbstractAuthenticationToken authRequest;
switch (loginType.toLowerCase()) {
case "email":
authRequest = new EmailPasswordAuthenticationToken(username, password);
break;
case "phone":
authRequest = new PhonePasswordAuthenticationToken(username, password);
break;
default:
authRequest = new UsernamePasswordAuthenticationToken(username, password);
break;
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Security 配置
@Configuration
@EnableWebSecurity
public class MultiProviderSecurityConfig {
@Autowired
private UsernameAuthenticationProvider usernameAuthenticationProvider;
@Autowired
private EmailAuthenticationProvider emailAuthenticationProvider;
@Autowired
private PhoneAuthenticationProvider phoneAuthenticationProvider;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
ProviderManager providerManager = new ProviderManager(
Arrays.asList(usernameAuthenticationProvider, emailAuthenticationProvider, phoneAuthenticationProvider)
);
providerManager.setEraseCredentialsAfterAuthentication(true);
return providerManager;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MultiAccountAuthenticationFilter authFilter = new MultiAccountAuthenticationFilter();
authFilter.setAuthenticationManager(authenticationManager());
authFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/dashboard"));
authFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"));
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.addFilterAt(authFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin(form -> form.disable()) // 禁用默认的表单登录
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
登录页面 HTML
<!DOCTYPE html>
<html>
<head>
<title>多账号登录</title>
</head>
<body>
<h2>用户登录</h2>
<form action="/login" method="post">
<div>
<label>
<input type="radio" name="loginType" value="username" checked> 用户名
</label>
<label>
<input type="radio" name="loginType" value="email"> 邮箱
</label>
<label>
<input type="radio" name="loginType" value="phone"> 手机号
</label>
</div>
<div>
<input type="text" name="username" placeholder="请输入用户名/邮箱/手机号" required>
</div>
<div>
<input type="password" name="password" placeholder="请输入密码" required>
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>
这种多 AuthenticationProvider 的方案具有以下特点:
- 完全解耦:每种登录方式的认证逻辑完全独立
- 安全策略定制:可以为不同的登录方式配置不同的安全策略
- 错误信息精准:可以根据不同的登录方式返回不同的错误信息
- 扩展性强:添加新的登录方式只需要添加新的
AuthenticationProvider和Authentication对象
基于 JWT 的多账号登录实现
在现代 Web 应用中,特别是前后端分离的架构中,JWT(JSON Web Token)已经成为主流的认证方案。让我们看看如何在 JWT 架构下实现多账号登录。
JWT 工具类
@Component
public class JwtUtil {
private String secret = "mySecretKeyForJWTGenerationThatShouldBeLongEnough";
private int jwtExpirationMs = 86400000; // 24小时
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
JWT 请求过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private MultiAccountUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
JWT 认证控制器
@RestController
@RequestMapping("/api/auth")
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MultiAccountUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest)
throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("用户名或密码错误", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
@PostMapping("/register")
public ResponseEntity<?> saveUser(@RequestBody UserDto userDto) throws Exception {
return ResponseEntity.ok(userDetailsService.save(userDto));
}
}
// DTO 类
@Data
public class JwtRequest {
private String username;
private String password;
}
@Data
public class JwtResponse {
private final String token;
public JwtResponse(String token) {
this.token = token;
}
}
@Data
public class UserDto {
private String username;
private String email;
private String phone;
private String password;
}
Security 配置(JWT 版本)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
在 JWT 架构下,多账号登录的实现相对简单,因为我们只需要在 UserDetailsService 中处理多种账号类型的识别即可。JWT 本身不关心用户是通过什么方式登录的,它只关心用户名和对应的权限。
多租户多账号登录实现
在 SaaS(Software as a Service)应用中,多租户架构是常见的需求。每个租户都有自己的用户体系,同一个用户名在不同租户下可能代表不同的用户。这种场景下的多账号登录需要考虑租户标识。
数据库设计(多租户)
-- 租户表
CREATE TABLE tenants (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_code VARCHAR(50) UNIQUE NOT NULL,
tenant_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 用户表(包含租户ID)
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tenant_username (tenant_id, username),
UNIQUE KEY uk_tenant_email (tenant_id, email),
UNIQUE KEY uk_tenant_phone (tenant_id, phone),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
租户上下文
@Component
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) {
currentTenant.set(tenant);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}













