本文講一下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, 權限校驗成功