本文講一下spring security關於許可權認證相關的內容
spring security 過濾器鏈
先來講一下spring security的工作過程。它其實就是一系列的filter過濾器和攔截器。
我們最常用的一般是身份認證過濾器過濾器: usernamePassword Authentication Filter,以及今天要講到的許可權攔截器 FilterSecuity Interceptor。
以下是完整的過濾器鏈, 但是我們並不需要完全關心所有的。
Spring Security的核心邏輯都在這一套過濾器中,過濾器裡會呼叫各種元件完成功能,掌握了這些過濾器和元件我們就基本掌握了Spring Security,這個框架的使用方式就是對這些過濾器和元件進行擴充套件。
UsernamePasswordAuthenticationFilter
我們先來簡單回顧一下使用者認證,因為我們需要的許可權資訊,需要從 Authentication 中獲取。
Authentication 是什麼呢?這裡簡單介紹一下
裡面最重要的有三項資訊:
- Principal:使用者資訊,沒有認證時一般是使用者名稱,認證後一般是使用者物件
- Credentials:使用者憑證,一般是密碼
- Authorities:使用者許可權
而 Authentication 就代表 當前登入使用者
首先 在 UsernamePasswordAuthenticationFilter
中,將使用者名稱密碼封裝成UsernamePasswordAuthenticationToken。並呼叫authenticate方法認證
而 authenticate 方法由 AuthenticationManager 提供, 是Spring Security用於執行身份驗證的元件,只需要呼叫它的authenticate 方法即可完成認證
authenticate方法的大概邏輯:
- this.getUserDetailsService().loadUserByUsername(username); 獲取 UserDtails類
- 呼叫passwordEncoder.matches(password, userDetails.getPassword()判斷使用者名稱密碼是否相同
- 返回的已認證Authentication,將整個UserDetails放進去充當Principal
所以我們的目的就很清楚了: 我們自己實現UserDetialsService、UserDetails、PasswordEncoder,這三個元件/類
UserDetialsService
自定義UserDetailService,重寫 loadByUsername,獲取使用者資訊。
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("使用者不存在"));
user.getAuthorities();
return user;
}
}
UserDetials類
可以注意到的是 loadUserByUsername 返回型別是 UserDetails
,這是很重要的一個類。因為我們的許可權就是透過該類的 getAuthorities()
方法獲取的.
// UserDetails介面方法
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
}
所以我們的許可權哪裡來? 就是透過返回 UserDetails
型別的物件,spring security就可以呼叫 getAuthorities 來獲取我們的許可權。
如何返回這個 UserDetails
型別的物件?
第一種方案: 我們直接 new 一個 spring security 實現 UserDetails
介面的 類。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("使用者不存在"));
// 設定使用者角色
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 這裡替換成你獲取許可權的方法
authorities.add(new SimpleGrantedAuthority("admin"));
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities);
}
第二種方案: 我們自己的類,繼承 UserDetails 類,並重寫其中的方法
public class User implements UserDetails {
@Override
Collection<? extends GrantedAuthority> getAuthorities() {
// 返回許可權
}
}
現在的一般都是基於RBAC(Role-Based Access Control)模型來進行許可權控制,即:基於角色的許可權控制。
所以上面的程式碼可以替換成此邏輯: 獲取使用者所有角色,獲取對應角色對應的所有許可權,返回所有許可權。
至此,我們已經成功返回了一個 UserDetails
型別的物件,且其中有我們的許可權資訊。
PasswordEncoder
可用 自帶的 BCryptPasswordEncoder
@Configuration
@EnableWebSecurity
public class MvcSecurityConfig extends WebSecurityConfigurerAdapter {
private final BCryptPasswordEncoder passwordEncoder;
public MvcSecurityConfig() {
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Bean
PasswordEncoder passwordEncoder() {
return this.passwordEncoder;
}
}
經過上述校驗完後, 我們獲得了一個 UsernamePasswordAuthenticationToken 型別的物件,其中有我們的使用者名稱,密碼,許可權
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(result);
之後就會存到我們的 SecurityContextHolder 安全上下文中。
至此,我們已經走完了 UsernamePasswordAuthenticationFilter。當前登入使用者已經存在 SecurityContextHolder 中, 登出或者session過期前我們都不需要重新認證。直接從上下文中獲取就可以。
FilterSecuity Interceptor
啟動許可權認證
修改WebSecurityConfig類
配置類新增註解:
開啟基於方法的安全認證機制,也就是說在web層的controller啟用註解機制的安全確認
@EnableGlobalMethodSecurity(prePostEnabled = true)
至此我們就可以在controller層使用 @PreAuthorize 進行校驗了。
@RestController
@RequestMapping("/V1.0/syllabus")
@PreAuthorize("hasAuthority('SCOPE_all')")
public class ApiSyllabusController {
}
表示訪問該Controller下的所有方法,都需要當前登入使用者有 SCOPE_all許可權。
我們來簡單看一下 hasAuthority 方法
呼叫了 hasAnyAuthorityName
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
從 getAuthoritySet()方法中獲取所有許可權, 然後判斷 SCOPE_all 是否在 Set<String> 中, 如果是,則證明當前登入使用者有 SCOPE_all 許可權,允許訪問。
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
獲取許可權方法:
private Set<String> getAuthoritySet() {
if (this.roles == null) {
Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
if (this.roleHierarchy != null) {
userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
}
this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return this.roles;
}
重點是這行, 看到了我們熟悉的東西 this.authentication.getAuthorities()
Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
所以原理很簡單,我們之間將包含許可權的 UserDetails 封裝在 authentication中。直接呼叫 getAuthorities() 方法就能獲取當前登入使用者所有許可權了。
@PreAuthorize("hasAuthority('SCOPE_all')")
至此 hasAuthority 返回了 true, 許可權校驗成功