首页 > 编程开发 > Java    日期:2026-06-18 / 浏览

引言

在现代企业应用和互联网平台中,用户身份认证的需求日益复杂。传统的单一用户名密码登录方式已经无法满足多样化的业务场景。用户可能需要通过手机号、邮箱、用户名、第三方社交账号等多种方式进行登录,甚至同一个用户可能拥有多个不同类型的账号。这种多账号登录的需求催生了对灵活、可扩展的身份认证架构的迫切需求。

Spring Security 作为 Java 生态中最成熟、最强大的安全框架,提供了丰富的扩展点和灵活的配置选项,能够很好地支持多账号登录的实现。本文将深入探讨如何基于 Spring Security 构建一个支持多账号登录的认证系统,从基础概念到高级实现,从简单配置到复杂场景,全方位解析多账号登录的技术要点和最佳实践。

多账号登录的业务场景分析

在开始技术实现之前,我们需要明确多账号登录的具体业务场景和需求。不同的业务场景对应着不同的技术实现方案。

常见的多账号类型

  1. 用户名/邮箱/手机号混合登录:用户可以使用注册时的用户名、绑定的邮箱或手机号中的任意一种方式进行登录
  2. 多租户账号体系:不同租户下的用户可能使用相同的用户名,但属于不同的租户空间
  3. 第三方账号集成:除了本地账号外,还支持微信、QQ、GitHub 等第三方账号登录
  4. 企业内部多系统账号:员工可能拥有多个系统的账号,需要统一认证
  5. 角色分离账号:同一个用户在不同业务场景下使用不同的身份(如管理员账号和个人账号)

技术挑战

实现多账号登录面临的主要技术挑战包括:

  • 账号唯一性保证:如何确保不同类型的账号标识符不会冲突
  • 认证逻辑复杂化:需要根据不同的登录方式执行不同的认证逻辑
  • 用户体验一致性:无论使用哪种方式登录,都应该提供一致的用户体验
  • 安全性考虑:不同登录方式的安全级别可能不同,需要相应的安全策略
  • 数据模型设计:如何设计数据库表结构来支持多账号体系

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 的实现即可。但是它也存在一些局限性:

  1. 账号类型判断逻辑耦合:账号类型的判断逻辑与业务逻辑耦合在一起
  2. 扩展性有限:如果需要添加新的登录方式,需要修改现有的判断逻辑
  3. 性能问题:每次登录都需要进行多次数据库查询来确定账号类型
  4. 安全性考虑不足:没有针对不同登录方式实施不同的安全策略

基于策略模式的多账号登录实现

为了提高代码的可维护性和扩展性,我们可以使用策略模式来实现多账号登录。

策略接口定义

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 的方案具有以下特点:

  • 完全解耦:每种登录方式的认证逻辑完全独立
  • 安全策略定制:可以为不同的登录方式配置不同的安全策略
  • 错误信息精准:可以根据不同的登录方式返回不同的错误信息
  • 扩展性强:添加新的登录方式只需要添加新的 AuthenticationProviderAuthentication 对象

基于 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();
    }
}

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章