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;
}我們可以看到,我們正在考慮用户與角色之間的關係以及角色與權限之間的關係都為多對多雙向關係。
3. 設置權限和角色
接下來,我們將重點關注在系統中設置權限和角色的早期配置。
我們將將其與應用程序的啓動關聯起來,並使用 ApplicationListener 在 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 中,我們的權限被稱為 Role 並且也稱為 (granted) authority,這有點令人困惑。
這對於實現來説不是問題,當然,但絕對值得注意。
其次,這些 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. 角色層級
此外,我們還可以將角色組織成層級結構。
我們已經瞭解瞭如何通過將權限映射到角色來實現基於角色的訪問控制。這允許我們為用户分配單個角色,而無需為每個單獨的權限進行分配。
然而,隨着角色的數量增加,用户可能會需要多個角色,從而導致角色爆炸(Role Explosion):
為了克服這個問題,我們可以使用 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 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 支持的系統。
我們還配置了角色層次結構,以簡化我們的訪問控制配置。