Spring Security – 角色与权限
上次更新:2024 年 1 月 8 日
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;
}
正如我们所见,我们将 User <-> Role 以及 Role <-> Privilege 关系视为 多对多双向关系。
3. 设置权限和角色
接下来,让我们专注于在系统中进行一些权限和角色的早期设置。
我们将将其与应用程序的启动绑定,并将ApplicationListener on 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("[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;
}
}
那么,这段简单的设置代码中发生了什么?没什么复杂的
- 我们正在创建权限。
- 然后我们创建角色并将权限分配给它们。
- 最后,我们创建一个用户并分配一个角色给它。
请注意,我们正在使用alreadySetup标志来确定是否需要运行设置。 仅仅是因为ContextRefreshedEvent 可能会根据我们应用程序中配置的上下文数量多次触发。我们只想运行一次设置。
这里有两个快速说明。 我们将首先关注术语。 我们在这里使用权限 – 角色术语。但在 Spring 中,这些略有不同。 在 Spring 中,我们的权限被称为角色,也称为(授予的)权限,这有点令人困惑。
这当然对实现没有问题,但绝对值得注意。
其次,这些 Spring 角色(我们的权限)需要一个前缀。 默认情况下,该前缀是“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 实体。
这种映射使整个安全配置高度灵活和强大。 我们可以根据需要混合和匹配角色和权限,最终,它们将被正确地映射到权限并返回到框架。
5. 角色层次结构
此外,让我们将我们的角色组织成层次结构。
我们已经了解了如何通过将权限映射到角色来实现基于角色的访问控制。这允许我们向用户分配单个角色,而不是必须分配所有单个权限。
然而,随着角色数量的增加,用户可能需要多个角色,从而导致角色爆炸。
为了克服这个问题,我们可以使用 Spring Security 的角色层次结构。
分配 角色ADMIN 会自动赋予用户STAFF 和 USER 角色所具有的权限。
然而,具有 STAFF 角色的用户只能执行 STAFF 和 USER 角色的操作。
让我们通过简单地暴露一个 RoleHierarchy 类型的 bean 来在 Spring Security 中创建这个层次结构。
@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. 用户注册
最后,让我们看一下新用户的注册。
我们已经了解了如何设置创建 User 并向其分配角色(和权限)。
现在让我们看看这在注册新用户时需要如何完成。
@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 支持的系统中,使用 JPA 实现角色和权限。
我们还配置了一个角色层次结构,以简化我们的访问控制配置。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















