博客 / 詳情

返回

spring security權限認證

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.