Spring Security - 角色和权限

1、概览

本文将带你了解如何在 Spring Security 中正确地实现 角色(Role)权限(Privilege)

2、用户、角色和权限

有如下 3 个实体:

  • User:代表用户
  • Role:代表用户在系统中的高级角色。每个角色都有一组低级权限。
  • Privilege:代表系统中较低级别的、细粒度的特权/权限。

User 如下:

@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;
}

如上,用户包含角色和一些额外的细节,这些细节对于适当的注册机制来说是必要的。

Role 如下:

@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;
}

最后是 Privilege

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

    private String name;

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

可以看到,用户 <-> 角色和角色 <-> 权限关系都是多对多的双向关系。

3、设置权限和角色

接下来,对系统中的权限和角色进行一些初始设置。监听 ContextRefreshedEvent 事件,在服务器启动时加载初始数据:

@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("test@test.com");
        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;
    }
}

如上:

  • 创建权限
  • 创建角色并为其分配权限
  • 最后,创建用户,并为其分配一个角色

注意,这里使用 alreadySetup 标志来确定是否需要运行设置。这只是因为 ContextRefreshedEvent 可能会被触发多次,这取决于在应用中配置了多少 Context。而我们只想运行一次设置。

这里有两点需要注意。首先来看术语。在这里使用了 “Privilege”(权限)和 “Role”(角色)这两个术语。但是在 Spring 中,它们略有不同。在 Spring 中,“Privilege” 被称为 “Role”,同时也被称为 “authority”,这可能会有些混淆。

其次,这些 Spring Role(本例中的 Privilege)需要一个前缀。默认情况下,前缀是 “ROLE”,但也可以更改。在这里,为了保持简单,没有使用该前缀,但注意,如果没有明确更改它,它是必需的。

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 实体的。

这种映射使整个 Security 配置高度灵活、功能强大。可以根据需要对角色和权限进行细化混合和匹配,最后将它们正确映射到权限集合并返回到框架中。

5、角色分层

上文介绍了如何通过将权限映射到角色来实现基于角色的访问控制。这样,就可以为用户分配一个角色,而不必分配所有单独的权限。

然而,随着角色数量的增加,用户可能需要多个角色,从而导致角色爆炸:

角色爆炸

为了解决这个问题,可以使用 Spring Security 的角色分层:

角色分层

分配角色 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 表达式中加入角色层次结构,需要在 WebSecurityExpressionHandler 中添加 roleHierarchy 实例:

@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")
    ...
}

你可以看到,角色分层是减少需要为用户添加的角色和权限数量的好方法。

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、总结

本文介绍了如何在 Spring Security 中正确地实现角色(Role)和权限(Privilege),以及如何通过角色层次结构来简化访问控制。


Ref:https://www.baeldung.com/role-and-privilege-for-spring-security-registration