Spring Security多賬號登錄的配置與實現(xiàn)方案
引言
在現(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)方案。
常見的多賬號類型
- 用戶名/郵箱/手機號混合登錄:用戶可以使用注冊時的用戶名、綁定的郵箱或手機號中的任意一種方式進行登錄
- 多租戶賬號體系:不同租戶下的用戶可能使用相同的用戶名,但屬于不同的租戶空間
- 第三方賬號集成:除了本地賬號外,還支持微信、QQ、GitHub 等第三方賬號登錄
- 企業(yè)內(nèi)部多系統(tǒng)賬號:員工可能擁有多個系統(tǒng)的賬號,需要統(tǒng)一認(rèn)證
- 角色分離賬號:同一個用戶在不同業(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)即可。但是它也存在一些局限性:
- 賬號類型判斷邏輯耦合:賬號類型的判斷邏輯與業(yè)務(wù)邏輯耦合在一起
- 擴展性有限:如果需要添加新的登錄方式,需要修改現(xiàn)有的判斷邏輯
- 性能問題:每次登錄都需要進行多次數(shù)據(jù)庫查詢來確定賬號類型
- 安全性考慮不足:沒有針對不同登錄方式實施不同的安全策略
基于策略模式的多賬號登錄實現(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ù)不同的登錄方式返回不同的錯誤信息
- 擴展性強:添加新的登錄方式只需要添加新的
AuthenticationProvider和Authentication對象
基于 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)文章
Java實現(xiàn)人臉識別登錄、注冊等功能(最新完整版)
這段時間由于學(xué)校實行靜態(tài)化管理,寢室門和校門都是用了人臉識別的裝置,本系列項目從設(shè)計到實現(xiàn)源碼全部開源免費學(xué)習(xí)使用,對Java實現(xiàn)人臉識別登錄、注冊功能感興趣的朋友一起看看吧2022-05-05
SpringCloud?Gateway讀取Request?Body方式
這篇文章主要介紹了SpringCloud?Gateway讀取Request?Body方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
IDEA報錯Error?running‘Application‘:Command?line?is?too?lo
這篇文章主要介紹了IDEA報錯Error?running?‘Application‘:Command?line?is?too?long的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06

