Spring Security – 角色與權限

Spring Security
Remote
1
08:01 PM · Nov 29 ,2025

1. 概述

本教程延續了使用 Spring Security 進行註冊系列,重點介紹瞭如何正確實現 角色和權限

2. 用户、角色和權限

我們先來看我們的實體。我們有三個主要實體:

  • 用户
  • 角色代表用户在系統中的高級角色。每個角色將具有一組低級別的權限。
  • 權限代表系統中的低級別、粒度細化的權限/授權。

以下是用户

@Entity
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;
    private String password;
    private boolean enabled;
    private boolean tokenExpired;

    @ManyToMany 
    @JoinTable( 
        name = "users_roles", 
        joinColumns = @JoinColumn(
          name = "user_id", referencedColumnName = "id"), 
        inverseJoinColumns = @JoinColumn(
          name = "role_id", referencedColumnName = "id")) 
    private Collection<Role> roles;
}

如你所見,用户包含角色以及用於適當註冊機制的幾個附加詳細信息。

接下來,以下是角色

@Entity
public class Role {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    @ManyToMany(mappedBy = "roles")
    private Collection<User> users;

    @ManyToMany
    @JoinTable(
        name = "roles_privileges", 
        joinColumns = @JoinColumn(
          name = "role_id", referencedColumnName = "id"), 
        inverseJoinColumns = @JoinColumn(
          name = "privilege_id", referencedColumnName = "id"))
    private Collection<Privilege> privileges;
}

最後,讓我們看一下權限

@Entity
public class Privilege {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "privileges")
    private Collection<Role> roles;
}

如你所見,我們正在考慮用户 <-> 角色以及角色 <-> 權限之間的關係是多對多雙向關係

接下來,讓我們專注於在系統中設置權限和角色的早期配置。

我們將將其與應用程序的啓動關聯起來,並且我們將使用一個<em>ApplicationListener</em>在<em>ContextRefreshedEvent</em>上加載初始數據在服務器啓動時:

@Component
public class SetupDataLoader implements
  ApplicationListener<ContextRefreshedEvent> {

    boolean alreadySetup = false;

    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private RoleRepository roleRepository;
 
    @Autowired
    private PrivilegeRepository privilegeRepository;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Override
    @Transactional
    public void onApplicationEvent(ContextRefreshedEvent event) {
 
        if (alreadySetup)
            return;
        Privilege readPrivilege
          = createPrivilegeIfNotFound("READ_PRIVILEGE");
        Privilege writePrivilege
          = createPrivilegeIfNotFound("WRITE_PRIVILEGE");
 
        List<Privilege> adminPrivileges = Arrays.asList(
          readPrivilege, writePrivilege);
        createRoleIfNotFound("ROLE_ADMIN", adminPrivileges);
        createRoleIfNotFound("ROLE_USER", Arrays.asList(readPrivilege));

        Role adminRole = roleRepository.findByName("ROLE_ADMIN");
        User user = new User();
        user.setFirstName("Test");
        user.setLastName("Test");
        user.setPassword(passwordEncoder.encode("test"));
        user.setEmail("[email protected]");
        user.setRoles(Arrays.asList(adminRole));
        user.setEnabled(true);
        userRepository.save(user);

        alreadySetup = true;
    }

    @Transactional
    Privilege createPrivilegeIfNotFound(String name) {
 
        Privilege privilege = privilegeRepository.findByName(name);
        if (privilege == null) {
            privilege = new Privilege(name);
            privilegeRepository.save(privilege);
        }
        return privilege;
    }

    @Transactional
    Role createRoleIfNotFound(
      String name, Collection<Privilege> privileges) {
 
        Role role = roleRepository.findByName(name);
        if (role == null) {
            role = new Role(name);
            role.setPrivileges(privileges);
            roleRepository.save(role);
        }
        return role;
    }
}

所以,這段簡單的設置代碼中發生了什麼?沒什麼複雜:

請注意,我們正在使用一個<em>alreadySetup</em>標誌來<strong>確定是否需要運行設置或不。</strong>這僅僅是因為<em>ContextRefreshedEvent</em>可能由於我們配置了多少上下文而多次觸發。我們只想運行設置一次。

這裏有兩個快速説明。我們首先將查看<strong>術語</strong>。我們在這裏使用“權限-角色”術語。但是,在 Spring 中,這些術語略有不同。在 Spring 中,我們的權限被稱為角色,也稱為(授予的)權限,這有點令人困惑。

這對於實現來説不是問題,但絕對值得注意。

4. 自定義 UserDetailsService

現在讓我們來查看認證過程。

我們將查看如何在自定義的 UserDetailsService中檢索用户,以及如何從用户分配的角色和權限中映射正確的權限集:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private IUserService service;
 
    @Autowired
    private MessageSource messages;
 
    @Autowired
    private RoleRepository roleRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException {
 
        User user = userRepository.findByEmail(email);
        if (user == null) {
            return new org.springframework.security.core.userdetails.User(
              " ", " ", true, true, true, true,
              getAuthorities(Arrays.asList(
                roleRepository.findByName("ROLE_USER"))));
        }

        return new org.springframework.security.core.userdetails.User(
          user.getEmail(), user.getPassword(), user.isEnabled(), true, true,
          true, getAuthorities(user.getRoles()));
    }

    private Collection<? extends GrantedAuthority> getAuthorities(
      Collection<Role> roles) {
 
        return getGrantedAuthorities(getPrivileges(roles));
    }

    private List<String> getPrivileges(Collection<Role> roles) {
 
        List<String> privileges = new ArrayList<>();
        List<Privilege> collection = new ArrayList<>();
        for (Role role : roles) {
            privileges.add(role.getName());
            collection.addAll(role.getPrivileges());
        }
        for (Privilege item : collection) {
            privileges.add(item.getName());
        }
        return privileges;
    }

    private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String privilege : privileges) {
            authorities.add(new SimpleGrantedAuthority(privilege));
        }
        return authorities;
    }
}

有趣的是,這裏跟蹤的是如何將權限(和角色)映射到 GrantedAuthority 實體。

這種映射使整個安全配置 高度靈活和強大。我們可以混合匹配角色和權限,只要粒度足夠細,在最後,它們將被正確映射到權限並返回到框架中。

5. 角色層級

此外,讓我們將角色組織成層級關係。

我們已經瞭解瞭如何通過將權限映射到角色來實施基於角色的訪問控制。這允許我們為用户分配單個角色,而無需分配所有單個權限。

然而,隨着角色的數量增加,用户可能需要多個角色,從而導致角色爆炸:

角色爆炸

為了克服這個問題,我們可以使用 Spring Security 的角色層級:

role-h

分配 角色 ADMIN 自動賦予用户 STAFFUSER 角色中的權限。

然而,具有 角色 STAFF 的用户只能執行 STAFFUSER 角色中的操作。

讓我們在 Spring Security 中通過簡單地暴露一個類型為 RoleHierarchy 的 Bean 來創建此層級關係:

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_ADMIN > ROLE_STAFF \n ROLE_STAFF > ROLE_USER";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

我們使用 > 符號在表達式中定義角色層級關係。這裏,我們已配置 ADMIN 角色包含 STAFF 角色,而 STAFF 角色又包含 USER 角色。

要將此角色層級關係包含在 Spring Web Expressions 中,我們需要將 roleHierarchy 實例添加到 WebSecurityExpressionHandler 中:

@Bean
public DefaultWebSecurityExpressionHandler customWebSecurityExpressionHandler() {
    DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
    expressionHandler.setRoleHierarchy(roleHierarchy());
    return expressionHandler;
}

最後,將 expressionHandler 添加到 http.authorizeRequests():

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
            .disable()
            .authorizeRequests()
                .expressionHandler(webSecurityExpressionHandler())
                .antMatchers(HttpMethod.GET, "/roleHierarchy")
                .hasRole("STAFF")
    ...
}

/roleHierarchy 端點使用 ROLE_STAFF 進行保護,以證明 webSecurityExpressionHandler 正在正常工作。

正如我們所看到的,角色層級關係是一種減少需要為用户添加的角和權限數量的好方法。

6. 用户註冊最後,讓我們來查看一下新用户的註冊過程。

我們已經看到如何通過設置來創建用户並分配角色(和權限)給它。

現在,讓我們來查看一下在註冊新用户時如何執行此操作:

@Override
public User registerNewUserAccount(UserDto accountDto) throws EmailExistsException {
 
    if (emailExist(accountDto.getEmail())) {
        throw new EmailExistsException
          ("There is an account with that email adress: " + accountDto.getEmail());
    }
    User user = new User();

    user.setFirstName(accountDto.getFirstName());
    user.setLastName(accountDto.getLastName());
    user.setPassword(passwordEncoder.encode(accountDto.getPassword()));
    user.setEmail(accountDto.getEmail());

    user.setRoles(Arrays.asList(roleRepository.findByName("ROLE_USER")));
    return repository.save(user);
}

在這一簡單實現中,由於我們假設正在註冊一個標準用户,因此我們為其分配了 ROLE_USER 角色。

當然,可以通過使用多個硬編碼的註冊方法或允許客户端發送正在註冊的用户的類型,輕鬆地在相同的方式中實現更復雜的邏輯。

7. 結論

在本文中,我們演示瞭如何使用 JPA 實現角色和權限,用於 Spring Security 支持的系統。

我們還配置了角色層次結構,以簡化我們的訪問控制配置。

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

發佈 評論

Some HTML is okay.