最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring Security多賬號登錄的配置與實現(xiàn)方案

 更新時間:2026年06月18日 08:40:59   作者:知遠(yuǎn)漫談  
在現(xiàn)代企業(yè)應(yīng)用和互聯(lián)網(wǎng)平臺中,用戶身份認(rèn)證的需求日益復(fù)雜,傳統(tǒng)的單一用戶名密碼登錄方式已經(jīng)無法滿足多樣化的業(yè)務(wù)場景,本文將深入探討如何基于 Spring Security 構(gòu)建一個支持多賬號登錄的認(rèn)證系統(tǒng),需要的朋友可以參考下

引言

在現(xiàn)代企業(yè)應(yīng)用和互聯(lián)網(wǎng)平臺中,用戶身份認(rèn)證的需求日益復(fù)雜。傳統(tǒng)的單一用戶名密碼登錄方式已經(jīng)無法滿足多樣化的業(yè)務(wù)場景。用戶可能需要通過手機號、郵箱、用戶名、第三方社交賬號等多種方式進行登錄,甚至同一個用戶可能擁有多個不同類型的賬號。這種多賬號登錄的需求催生了對靈活、可擴展的身份認(rèn)證架構(gòu)的迫切需求。

Spring Security 作為 Java 生態(tài)中最成熟、最強大的安全框架,提供了豐富的擴展點和靈活的配置選項,能夠很好地支持多賬號登錄的實現(xiàn)。本文將深入探討如何基于 Spring Security 構(gòu)建一個支持多賬號登錄的認(rèn)證系統(tǒng),從基礎(chǔ)概念到高級實現(xiàn),從簡單配置到復(fù)雜場景,全方位解析多賬號登錄的技術(shù)要點和最佳實踐。

多賬號登錄的業(yè)務(wù)場景分析

在開始技術(shù)實現(xiàn)之前,我們需要明確多賬號登錄的具體業(yè)務(wù)場景和需求。不同的業(yè)務(wù)場景對應(yīng)著不同的技術(shù)實現(xiàn)方案。

常見的多賬號類型

  1. 用戶名/郵箱/手機號混合登錄:用戶可以使用注冊時的用戶名、綁定的郵箱或手機號中的任意一種方式進行登錄
  2. 多租戶賬號體系:不同租戶下的用戶可能使用相同的用戶名,但屬于不同的租戶空間
  3. 第三方賬號集成:除了本地賬號外,還支持微信、QQ、GitHub 等第三方賬號登錄
  4. 企業(yè)內(nèi)部多系統(tǒng)賬號:員工可能擁有多個系統(tǒng)的賬號,需要統(tǒng)一認(rèn)證
  5. 角色分離賬號:同一個用戶在不同業(yè)務(wù)場景下使用不同的身份(如管理員賬號和個人賬號)

技術(shù)挑戰(zhàn)

實現(xiàn)多賬號登錄面臨的主要技術(shù)挑戰(zhàn)包括:

  • 賬號唯一性保證:如何確保不同類型的賬號標(biāo)識符不會沖突
  • 認(rèn)證邏輯復(fù)雜化:需要根據(jù)不同的登錄方式執(zhí)行不同的認(rèn)證邏輯
  • 用戶體驗一致性:無論使用哪種方式登錄,都應(yīng)該提供一致的用戶體驗
  • 安全性考慮:不同登錄方式的安全級別可能不同,需要相應(yīng)的安全策略
  • 數(shù)據(jù)模型設(shè)計:如何設(shè)計數(shù)據(jù)庫表結(jié)構(gòu)來支持多賬號體系

Spring Security 核心組件回顧

在深入多賬號登錄實現(xiàn)之前,讓我們先回顧一下 Spring Security 的核心組件,這些組件將在我們的實現(xiàn)中發(fā)揮關(guān)鍵作用。

AuthenticationManager 認(rèn)證管理器

AuthenticationManager 是 Spring Security 認(rèn)證流程的核心接口。它負(fù)責(zé)接收 Authentication 對象并返回經(jīng)過認(rèn)證的 Authentication 對象。

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationProvider 認(rèn)證提供者

AuthenticationProvider 是具體的認(rèn)證邏輯實現(xiàn)。AuthenticationManager 通常會委托給一個或多個 AuthenticationProvider 來執(zhí)行實際的認(rèn)證工作。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

UserDetailsService 用戶詳情服務(wù)

UserDetailsService 負(fù)責(zé)根據(jù)用戶名加載用戶信息,返回 UserDetails 對象。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

AuthenticationFilter 認(rèn)證過濾器

認(rèn)證過濾器負(fù)責(zé)從請求中提取認(rèn)證信息,并創(chuàng)建 Authentication 對象提交給 AuthenticationManager。

基礎(chǔ)多賬號登錄實現(xiàn)方案

最簡單的多賬號登錄實現(xiàn)是在 UserDetailsService 中處理多種賬號類型的識別和查詢。

數(shù)據(jù)庫設(shè)計

首先,我們需要設(shè)計支持多賬號的數(shù)據(jù)庫表結(jié)構(gòu):

-- 用戶主表
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 實現(xiàn)

@Service
public class MultiAccountUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String loginIdentifier) throws UsernameNotFoundException {
        // 判斷登錄標(biāo)識符的類型
        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;
        }
        
        // 判斷是否為手機號(簡單判斷,實際項目中需要更嚴(yán)格的驗證)
        if (trimmed.matches("^1[3-9]\\d{9}$")) {
            return LoginType.PHONE;
        }
        
        // 默認(rèn)認(rèn)為是用戶名
        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 層實現(xiàn)

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
    User findByEmail(String email);
    User findByPhone(String phone);
}

Entity 層實現(xiàn)

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

這種基礎(chǔ)實現(xiàn)方案的優(yōu)點是簡單直接,只需要修改 UserDetailsService 的實現(xiàn)即可。但是它也存在一些局限性:

  1. 賬號類型判斷邏輯耦合:賬號類型的判斷邏輯與業(yè)務(wù)邏輯耦合在一起
  2. 擴展性有限:如果需要添加新的登錄方式,需要修改現(xiàn)有的判斷邏輯
  3. 性能問題:每次登錄都需要進行多次數(shù)據(jù)庫查詢來確定賬號類型
  4. 安全性考慮不足:沒有針對不同登錄方式實施不同的安全策略

基于策略模式的多賬號登錄實現(xiàn)

為了提高代碼的可維護性和擴展性,我們可以使用策略模式來實現(xiàn)多賬號登錄。

策略接口定義

public interface LoginStrategy {
    /**
     * 判斷當(dāng)前策略是否支持該登錄標(biāo)識符
     */
    boolean supports(String loginIdentifier);
    
    /**
     * 根據(jù)登錄標(biāo)識符查找用戶
     */
    User findUser(String loginIdentifier);
    
    /**
     * 獲取策略的優(yōu)先級(數(shù)值越小優(yōu)先級越高)
     */
    int getPriority();
}

具體策略實現(xiàn)

@Component
@Order(1)
public class UsernameLoginStrategy implements LoginStrategy {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public boolean supports(String loginIdentifier) {
        // 用戶名通常不包含特殊字符,且長度在合理范圍內(nèi)
        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();
    }
}

這種基于策略模式的實現(xiàn)具有以下優(yōu)勢:

  • 高內(nèi)聚低耦合:每種登錄方式的邏輯獨立封裝
  • 易于擴展:添加新的登錄方式只需要實現(xiàn)新的策略類
  • 靈活性強:可以通過調(diào)整優(yōu)先級來控制策略的執(zhí)行順序
  • 可測試性好:每個策略都可以獨立進行單元測試

多 AuthenticationProvider 方案

對于更復(fù)雜的場景,我們可以為每種登錄方式創(chuàng)建獨立的 AuthenticationProvider。這種方式提供了最大的靈活性,允許為不同的登錄方式配置不同的認(rèn)證邏輯和安全策略。

自定義 Authentication 對象

首先,我們需要為每種登錄方式創(chuàng)建對應(yīng)的 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"; // 默認(rèn)為用戶名登錄
        }
        
        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()) // 禁用默認(rèn)的表單登錄
            .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 的方案具有以下特點:

  • 完全解耦:每種登錄方式的認(rèn)證邏輯完全獨立
  • 安全策略定制:可以為不同的登錄方式配置不同的安全策略
  • 錯誤信息精準(zhǔn):可以根據(jù)不同的登錄方式返回不同的錯誤信息
  • 擴展性強:添加新的登錄方式只需要添加新的 AuthenticationProviderAuthentication 對象

基于 JWT 的多賬號登錄實現(xiàn)

在現(xiàn)代 Web 應(yīng)用中,特別是前后端分離的架構(gòu)中,JWT(JSON Web Token)已經(jīng)成為主流的認(rèn)證方案。讓我們看看如何在 JWT 架構(gòu)下實現(xiàn)多賬號登錄。

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 認(rèn)證控制器

@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 架構(gòu)下,多賬號登錄的實現(xiàn)相對簡單,因為我們只需要在 UserDetailsService 中處理多種賬號類型的識別即可。JWT 本身不關(guān)心用戶是通過什么方式登錄的,它只關(guān)心用戶名和對應(yīng)的權(quán)限。

多租戶多賬號登錄實現(xiàn)

在 SaaS(Software as a Service)應(yīng)用中,多租戶架構(gòu)是常見的需求。每個租戶都有自己的用戶體系,同一個用戶名在不同租戶下可能代表不同的用戶。這種場景下的多賬號登錄需要考慮租戶標(biāo)識。

數(shù)據(jù)庫設(shè)計(多租戶)

-- 租戶表
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();
    }
}

以上就是Spring Security多賬號登錄的配置與實現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于Spring Security多賬號登錄的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • JDK線程池和Spring線程池的使用實例解析

    JDK線程池和Spring線程池的使用實例解析

    這篇文章主要介紹了JDK線程池和Spring線程池的使用實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-11-11
  • Java實現(xiàn)人臉識別登錄、注冊等功能(最新完整版)

    Java實現(xiàn)人臉識別登錄、注冊等功能(最新完整版)

    這段時間由于學(xué)校實行靜態(tài)化管理,寢室門和校門都是用了人臉識別的裝置,本系列項目從設(shè)計到實現(xiàn)源碼全部開源免費學(xué)習(xí)使用,對Java實現(xiàn)人臉識別登錄、注冊功能感興趣的朋友一起看看吧
    2022-05-05
  • java 動態(tài)增加定時任務(wù)示例

    java 動態(tài)增加定時任務(wù)示例

    本篇文章主要介紹了java 動態(tài)增加定時任務(wù)示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-03-03
  • SpringCloud?Gateway讀取Request?Body方式

    SpringCloud?Gateway讀取Request?Body方式

    這篇文章主要介紹了SpringCloud?Gateway讀取Request?Body方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • Spring中的ConversionService源碼解析

    Spring中的ConversionService源碼解析

    這篇文章主要介紹了Spring中的ConversionService源碼解析,ConversionService是類型轉(zhuǎn)換服務(wù)的接口,從名字就可以看出ConverterRegistry是要實現(xiàn)轉(zhuǎn)換器注冊表的接口,添加和移除Converter和GenericConverter,需要的朋友可以參考下
    2023-11-11
  • IDEA報錯Error?running‘Application‘:Command?line?is?too?long的問題

    IDEA報錯Error?running‘Application‘:Command?line?is?too?lo

    這篇文章主要介紹了IDEA報錯Error?running?‘Application‘:Command?line?is?too?long的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-06-06
  • java多線程開啟的三種方式你知道嗎

    java多線程開啟的三種方式你知道嗎

    這篇文章主要為大家詳細(xì)介紹了java多線程開啟的三種方式,使用數(shù)據(jù)庫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下<BR>
    2022-02-02
  • 淺析Mybatis 在CS程序中的應(yīng)用

    淺析Mybatis 在CS程序中的應(yīng)用

    如果是自己用的Mybatis,不需要考慮對配置文件加密,如果不是,那就需要考慮加密,這篇文章主要講如何配置CS的Mybatis
    2013-07-07
  • Java源碼解析HashMap成員變量

    Java源碼解析HashMap成員變量

    今天小編就為大家分享一篇關(guān)于Java源碼解析HashMap成員變量,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • mybatis-plus 擴展批量新增的實現(xiàn)

    mybatis-plus 擴展批量新增的實現(xiàn)

    本文主要介紹了mybatis-plus 擴展批量新增的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01

最新評論

石门县| 东城区| 汝城县| 陇南市| 荔波县| 中西区| 陇西县| 同江市| 大足县| 日照市| 安陆市| 大姚县| 眉山市| 临夏县| 白河县| 西吉县| 商水县| 金湖县| 漳平市| 贵阳市| 伽师县| 邵阳市| 巴林左旗| 通许县| 诸城市| 葫芦岛市| 陈巴尔虎旗| 潜江市| 肥西县| 湘潭县| 广昌县| 西畴县| 科技| 西畴县| 那坡县| 阿鲁科尔沁旗| 凌云县| 明光市| 寻甸| 吉水县| 大余县|