基于Spring Security實(shí)現(xiàn)RBAC權(quán)限控制的完整步驟
什么是 RBAC
RBAC,全稱 Role-Based Access Control,基于角色的訪問控制。核心思路很簡(jiǎn)單:不直接給用戶分配權(quán)限,而是引入"角色"作為中間層。
用戶 →角色→ 權(quán)限
舉個(gè)例子:一家公司里,"實(shí)習(xí)生"能查看文檔,"正式員工"能查看和編輯文檔,"部門經(jīng)理"能查看、編輯、刪除文檔。你入職的時(shí)候,HR 不會(huì)一條條給你勾權(quán)限,而是直接給你一個(gè)角色,權(quán)限就跟著來了。
這里面有幾個(gè)關(guān)鍵關(guān)系:
- 一個(gè)用戶可以擁有多個(gè)角色
- 一個(gè)角色可以分配給多個(gè)用戶
- 一個(gè)角色可以包含多個(gè)權(quán)限
- 一個(gè)權(quán)限可以屬于多個(gè)角色
還有一種模型叫角色繼承——上層角色自動(dòng)繼承下層角色的所有權(quán)限。比如"經(jīng)理"自動(dòng)擁有"員工"的全部權(quán)限,再額外擁有管理權(quán)限。這在組織架構(gòu)復(fù)雜的系統(tǒng)中非常實(shí)用。
Spring Security 是什么
Spring Security 本質(zhì)上是一個(gè)基于 Filter 的安全框架。請(qǐng)求進(jìn)入應(yīng)用時(shí),會(huì)經(jīng)過一系列過濾器鏈,每個(gè)過濾器各司其職:有的負(fù)責(zé)認(rèn)證,有的負(fù)責(zé)授權(quán),有的負(fù)責(zé) CSRF 防護(hù)。
它解決兩個(gè)核心問題:
- 認(rèn)證(Authentication):你是誰?驗(yàn)證用戶名密碼是否正確。
- 授權(quán)(Authorization):你能干什么?檢查你有沒有權(quán)限訪問某個(gè)接口。
用 RBAC 的思路來說,認(rèn)證就是確認(rèn)你的身份,授權(quán)就是根據(jù)你的角色判斷你能不能進(jìn)這扇門。
整體架構(gòu)設(shè)計(jì)
要基于 Spring Security 實(shí)現(xiàn) RBAC,需要這幾個(gè)核心模塊:
config/ → 安全配置、JWT 過濾器 controller/ → 接收請(qǐng)求的入口 service/ → 業(yè)務(wù)邏輯、用戶詳情加載 repository/ → 數(shù)據(jù)庫(kù)訪問 model/ → 用戶實(shí)體、角色定義 utils/ → 密碼加密、JWT 工具
下面按照請(qǐng)求的生命周期來講,從注冊(cè)到登錄到鑒權(quán),一步步走通。
第一步:定義用戶模型和角色
@Data
@Entity
@Table(name = "wzxg_users", uniqueConstraints = @UniqueConstraint(columnNames = "username"))
public class WzxgUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private WzxgRole role;
@CreationTimestamp
private LocalDateTime createdTime;
@UpdateTimestamp
private LocalDateTime updatedTime;
public enum WzxgRole {
USER, ADMIN
}
}
@Enumerated(EnumType.STRING) 讓角色以字符串形式存到數(shù)據(jù)庫(kù)里,比如存的是 "ADMIN" 而不是 0,可讀性好,也不容易出錯(cuò)。
數(shù)據(jù)訪問層很簡(jiǎn)單,一個(gè)根據(jù)用戶名查詢的方法就夠了:
public interface WzxgUserRepository extends JpaRepository<WzxgUser, Long> {
Optional<WzxgUser> findByUsername(String username);
}
第二步:密碼加密
密碼絕對(duì)不能明文存儲(chǔ)。BCrypt 是目前主流的選擇,它內(nèi)置鹽值機(jī)制,同一個(gè)密碼每次加密結(jié)果都不同,能有效防御彩虹表攻擊。
public class WzxgPasswordUtil {
private static final BCryptPasswordEncoder wzxgEncoder = new BCryptPasswordEncoder();
public static String encode(String rawPassword) {
return wzxgEncoder.encode(rawPassword);
}
public static boolean matches(String rawPassword, String encodedPassword) {
return wzxgEncoder.matches(rawPassword, encodedPassword);
}
}
注冊(cè)時(shí)調(diào) encode(),登錄時(shí)調(diào) matches()
第三步:JWT 工具類
系統(tǒng)采用無狀態(tài)認(rèn)證,服務(wù)端不存 session,而是通過 JWT 令牌來傳遞身份信息。
@Component
public class WzxgJwtUtils {
private static final String WZXG_SECRET_KEY = "wzxg_jwt_secret_key_2024";
private static final long WZXG_EXPIRATION = 86400000; // 24小時(shí)
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + WZXG_EXPIRATION))
.signWith(SignatureAlgorithm.HS256, WZXG_SECRET_KEY)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(WZXG_SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public String extractUsername(String token) {
Claims wzxgClaims = Jwts.parser()
.setSigningKey(WZXG_SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return wzxgClaims.getSubject();
}
}
JWT 是一個(gè)自包含的令牌,里面可以塞用戶名、角色、組織信息等。這樣每次請(qǐng)求過來,解析 token 就能知道用戶是誰、有什么權(quán)限,不用查數(shù)據(jù)庫(kù)。
第四步:讓 Spring Security 認(rèn)識(shí)你的用戶
Spring Security 有自己的一套用戶模型 UserDetails,我們需要實(shí)現(xiàn) UserDetailsService 接口,把數(shù)據(jù)庫(kù)里的用戶轉(zhuǎn)換成它能理解的格式。
@Service
public class WzxgUserDetailsService implements UserDetailsService {
@Autowired
private WzxgUserRepository wzxgUserRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
WzxgUser wzxgUser = wzxgUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用戶不存在"));
return new org.springframework.security.core.userdetails.User(
wzxgUser.getUsername(),
wzxgUser.getPassword(),
buildAuthorities(wzxgUser.getRole())
);
}
private Collection<? extends GrantedAuthority> buildAuthorities(WzxgUser.WzxgRole role) {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
}
注意 buildAuthorities 方法里的 "ROLE_" 前綴,這是 Spring Security 的約定。角色 ADMIN 會(huì)變成 ROLE_ADMIN,這樣后面配置 hasRole("ADMIN") 時(shí)框架才能正確匹配。
第五步:JWT 認(rèn)證過濾器
這是整個(gè)流程的關(guān)鍵——每個(gè)請(qǐng)求進(jìn)來時(shí),過濾器從請(qǐng)求頭里取出 JWT,驗(yàn)證有效性,然后把用戶信息塞進(jìn) Spring Security 的上下文。
@Component
public class WzxgJwtAuthFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService wzxgUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String wzxgToken = getToken(request);//從請(qǐng)求頭獲取jwt token
if (wzxgToken != null) {
String username = getUsername(wzxgToken);
UserDetails wzxgUserDetails = wzxgUserDetailsService.loadUserByUsername(username);//根據(jù)用戶名加載用戶詳細(xì)信息
UsernamePasswordAuthenticationToken wzxgAuth =
new UsernamePasswordAuthenticationToken(
wzxgUserDetails, null, wzxgUserDetails.getAuthorities());
wzxgAuth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(wzxgAuth);
}
} catch (Exception e) {
}
filterChain.doFilter(request, response);
}
private String getToken(HttpServletRequest request) {
String wzxgBearer = request.getHeader("Authorization");
if (wzxgBearer != null && wzxgBearer.startsWith("Bearer ")) {
return wzxgBearer.substring(7);
}
return null;
}
private String getUsername(String token) {
try {
Claims claims = extractClaimsIgnoreExpiration(token);
return claims != null ? claims.getSubject() : null;
} catch (Exception e) {
return null;
}
}
}
繼承 OncePerRequestFilter 保證每個(gè)請(qǐng)求只過濾一次。如果 token 無效或不存在,不會(huì)報(bào)錯(cuò),只是不設(shè)置認(rèn)證信息,后續(xù)的權(quán)限檢查自然會(huì)攔住未認(rèn)證的請(qǐng)求。
第六步:安全配置——定義規(guī)則
最后把所有東西串起來,告訴 Spring Security:哪些接口誰能訪問。
@Configuration
@EnableWebSecurity
public class WzxgSecurityConfig {
@Autowired
private WzxgJwtAuthFilter wzxgJwtAuthFilter;
@Bean
public SecurityFilterChain wzxgFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// 登錄注冊(cè)不需要認(rèn)證
.requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll()
// 管理接口只有 ADMIN 能訪問
.requestMatchers("/api/v1/manage/**").hasRole("ADMIN")
// 普通業(yè)務(wù)接口,USER 和 ADMIN 都能訪問
.requestMatchers("/api/v1/workspace/**").hasAnyRole("USER", "ADMIN")
// 其余接口都需要認(rèn)證
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 把 JWT 過濾器插到 UsernamePasswordAuthenticationFilter 前面
.addFilterBefore(wzxgJwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
csrf.disable():因?yàn)橛玫氖?JWT 無狀態(tài)認(rèn)證,不需要 CSRF 防護(hù)SessionCreationPolicy.STATELESS:不創(chuàng)建 session,完全靠 tokenaddFilterBefore:確保 JWT 過濾器在 Spring Security 默認(rèn)的認(rèn)證過濾器之前執(zhí)行
完整請(qǐng)求流程
把上面的模塊串起來,一個(gè)請(qǐng)求的完整生命周期是這樣的:
1. 用戶注冊(cè) → 密碼用 BCrypt 加密 → 分配默認(rèn)角色 USER → 存入數(shù)據(jù)庫(kù) 2. 用戶登錄 → 驗(yàn)證用戶名密碼 → 生成 JWT Token → 返回給客戶端 3. 客戶端請(qǐng)求業(yè)務(wù)接口 → 請(qǐng)求頭帶上 Authorization: Bearer <token> 4. WzxgJwtAuthFilter 攔截請(qǐng)求 → 提取 token → 驗(yàn)證簽名和有效期 → 解析出用戶名 → 加載用戶信息和角色 → 寫入 SecurityContext 5. Spring Security 根據(jù) WzxgSecurityConfig 中的規(guī)則 → 檢查當(dāng)前用戶的角色是否匹配目標(biāo)接口的權(quán)限要求 → 通過則放行,否則返回 403
以上就是基于Spring Security實(shí)現(xiàn)RBAC權(quán)限控制的完整步驟的詳細(xì)內(nèi)容,更多關(guān)于Spring Security RBAC權(quán)限控制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java利用mybatis攔截器統(tǒng)計(jì)sql執(zhí)行時(shí)間示例
這篇文章主要介紹了java利用mybatis攔截器統(tǒng)計(jì)sql執(zhí)行時(shí)間示例,該攔截器攔截mybatis的query和update操作,能統(tǒng)計(jì)sql執(zhí)行時(shí)間2014-03-03
Java日期時(shí)間格式化操作DateUtils 的整理
這篇文章主要介紹了Java日期時(shí)間格式化操作DateUtils 的整理的相關(guān)資料,這里總結(jié)了java日期格式化的操作,需要的朋友可以參考下2017-07-07
Java?Swing實(shí)現(xiàn)自定義彈窗組件CusDialog(附源碼)
Swing 原生對(duì)話框 JDialog 和 JFrame 一樣,標(biāo)題欄是系統(tǒng)自帶的,樣式陳舊且無法自定義,下面我們就來看看如何通過Java Swing實(shí)現(xiàn)自定義彈窗組件CusDialog吧2026-05-05
JavaCV?本地視頻推流實(shí)現(xiàn)依賴示例
這篇文章主要為大家介紹了JavaCV?本地視頻推流實(shí)現(xiàn)的依賴示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Java中的拷貝數(shù)組CopyOnWriteArrayList詳解
這篇文章主要介紹了Java中的拷貝數(shù)組CopyOnWriteArrayList詳解,ArrayList和LinkedList都不是線程安全的,如果需要線程安全的List,可以使用synchronizedList來生成一個(gè)同步list,但是這個(gè)同步list的方法都是通過synchronized修飾來保證同步的,需要的朋友可以參考下2023-12-12
SpringBoot RedisTemplate分布式鎖的項(xiàng)目實(shí)戰(zhàn)
本文主要介紹了SpringBoot RedisTemplate分布式鎖的項(xiàng)目實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
淺談Java中常用數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)類 Collection和Map
下面小編就為大家?guī)硪黄獪\談Java中常用數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)類 Collection和Map。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-09-09

