spring security許可權認證

發表於2023-09-18

本文講一下spring security關於許可權認證相關的內容

spring security 過濾器鏈

先來講一下spring security的工作過程。它其實就是一系列的filter過濾器和攔截器。

我們最常用的一般是身份認證過濾器過濾器: usernamePassword Authentication Filter,以及今天要講到的許可權攔截器 FilterSecuity Interceptor

image.png

以下是完整的過濾器鏈, 但是我們並不需要完全關心所有的。
image.png

Spring Security的核心邏輯都在這一套過濾器中,過濾器裡會呼叫各種元件完成功能,掌握了這些過濾器和元件我們就基本掌握了Spring Security,這個框架的使用方式就是對這些過濾器和元件進行擴充套件。

UsernamePasswordAuthenticationFilter

我們先來簡單回顧一下使用者認證,因為我們需要的許可權資訊,需要從 Authentication 中獲取。

Authentication 是什麼呢?這裡簡單介紹一下

裡面最重要的有三項資訊:

  • Principal:使用者資訊,沒有認證時一般是使用者名稱,認證後一般是使用者物件
  • Credentials:使用者憑證,一般是密碼
  • Authorities:使用者許可權

而 Authentication 就代表 當前登入使用者


image.png

首先 在 UsernamePasswordAuthenticationFilter中,將使用者名稱密碼封裝成UsernamePasswordAuthenticationToken。並呼叫authenticate方法認證

image.png

而 authenticate 方法由 AuthenticationManager 提供, 是Spring Security用於執行身份驗證的元件,只需要呼叫它的authenticate 方法即可完成認證

authenticate方法的大概邏輯:

  1. this.getUserDetailsService().loadUserByUsername(username); 獲取 UserDtails類
  2. 呼叫passwordEncoder.matches(password, userDetails.getPassword()判斷使用者名稱密碼是否相同
  3. 返回的已認證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)模型來進行許可權控制,即:基於角色的許可權控制。

image.png

所以上面的程式碼可以替換成此邏輯: 獲取使用者所有角色,獲取對應角色對應的所有許可權,返回所有許可權。

至此,我們已經成功返回了一個 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, 許可權校驗成功

相關文章