1、概览 {#1概览}
本文将带你了解如何在 Spring Security 中正确地实现 角色(Role) 和 权限(Privilege)。
2、用户、角色和权限 {#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、设置权限和角色 {#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 {#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、角色分层 {#5角色分层}
上文介绍了如何通过将权限映射到角色来实现基于角色的访问控制。这样,就可以为用户分配一个角色,而不必分配所有单独的权限。
然而,随着角色数量的增加,用户可能需要多个角色,从而导致角色爆炸:
为了解决这个问题,可以使用 Spring Security 的角色分层:
分配角色 ADMIN 会自动赋予用户 STAFF 和 USER 两种角色的权限。
但是,具有 STAFF 角色的用户只能执行 STAFF 和 USER 角色的操作。
在 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、用户注册 {#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、总结 {#7总结}
本文介绍了如何在 Spring Security 中正确地实现角色(Role)和权限(Privilege),以及如何通过角色层次结构来简化访问控制。
Ref:https://www.baeldung.com/role-and-privilege-for-spring-security-registration