Перейти к основному содержимому

Spring Security — роли и привилегии

· 6 мин. чтения

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("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;
}
}

Итак, что же происходит во время выполнения этого простого кода установки? Ничего сложного:

  • Мы создаем привилегии.
  • Затем мы создаем роли и назначаем им привилегии.
  • Наконец, мы создаем пользователя и назначаем ему роль.

Обратите внимание, как мы используем флаг ужеSetup , чтобы определить, нужно ли запускать установку или нет. Это просто потому, что ContextRefreshedEvent может запускаться несколько раз в зависимости от того, сколько контекстов мы настроили в нашем приложении. И мы хотим запустить установку только один раз.

Две короткие заметки здесь. Сначала рассмотрим терминологию. Здесь мы используем термины « привилегия — роль ». Но весной они немного отличаются. В Spring наша привилегия называется ролью, а также (предоставленной) властью, что немного сбивает с толку.

Это не проблема для реализации конечно, но это определенно стоит отметить.

Во-вторых, этим Spring-ролям (нашим привилегиям) нужен префикс. По умолчанию этот префикс — «ROLE», но его можно изменить. Мы не используем этот префикс здесь, просто для простоты, но имейте в виду, что он потребуется, если мы не изменим его явно.

4. Пользовательская служба сведений о пользователе

Теперь давайте проверим процесс аутентификации.

Мы увидим, как получить пользователя в нашей пользовательской службе 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. Ролевая иерархия

Кроме того, давайте организуем наши роли в иерархии.

Мы увидели, как реализовать управление доступом на основе ролей, сопоставив привилегии с ролями. Это позволяет нам назначать пользователю одну роль вместо того, чтобы назначать все отдельные привилегии.

Однако по мере увеличения количества ролей пользователям может потребоваться несколько ролей , что приведет к взрывному росту числа ролей:

./d64e6dd05864bf3efc36755a8d4c0f0b.jpg

Чтобы преодолеть это, мы можем использовать иерархию ролей Spring Security:

./2aa0476c165b7c204b39d35252a69191.jpg

Назначение роли ADMIN автоматически дает пользователю привилегии ролей STAFF и USER .

Однако пользователь с ролью ПЕРСОНАЛ может выполнять только действия ролей ПЕРСОНАЛ и ПОЛЬЗОВАТЕЛЬ .

Давайте создадим эту иерархию в Spring Security, просто предоставив bean-компонент типа RoleHierarchy :

@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 , которая, в свою очередь, включает роль USER.

Наконец, чтобы включить эту иерархию ролей в Spring Web Expressions , мы добавляем экземпляр roleHierarchy в WebSecurityExpressionHandler :

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

Как мы видим, иерархия ролей — отличный способ уменьшить количество ролей и полномочий, которые нам нужно добавить пользователю.

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.

Мы также настроили иерархию ролей, чтобы упростить настройку контроля доступа.

Полную реализацию этого руководства по регистрации с помощью Spring Security можно найти на GitHub .