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

Добавление ролей и привилегий в приложение Reddit

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

1. Обзор

В этом выпуске мы представим простые роли и привилегии в нашем приложении Reddit , чтобы затем иметь возможность делать некоторые интересные вещи, такие как ограничение количества сообщений, которые обычный пользователь может планировать в Reddit ежедневно.

А так как у нас будет роль администратора — и неявно пользователь-администратор — мы также собираемся добавить область управления администратором.

2. Пользователь , роль и права доступа

Во-первых, мы изменим сущность пользователя , которую мы используем в нашей серии приложений Reddit, чтобы добавить роли:

@Entity
public class User {
...

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;

...
}

Обратите внимание, что отношения «пользователь-роль» являются гибкими «многие ко многим».

Далее мы собираемся определить сущности Role и Privilege . Для получения полной информации об этой реализации ознакомьтесь с этой статьей на ForEach .

3. Настройка

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

private void createRoles() {
Privilege adminReadPrivilege = createPrivilegeIfNotFound("ADMIN_READ_PRIVILEGE");
Privilege adminWritePrivilege = createPrivilegeIfNotFound("ADMIN_WRITE_PRIVILEGE");
Privilege postLimitedPrivilege = createPrivilegeIfNotFound("POST_LIMITED_PRIVILEGE");
Privilege postUnlimitedPrivilege = createPrivilegeIfNotFound("POST_UNLIMITED_PRIVILEGE");

createRoleIfNotFound("ROLE_ADMIN", Arrays.asList(adminReadPrivilege, adminWritePrivilege));
createRoleIfNotFound("ROLE_SUPER_USER", Arrays.asList(postUnlimitedPrivilege));
createRoleIfNotFound("ROLE_USER", Arrays.asList(postLimitedPrivilege));
}

И сделайте нашего тестового пользователя администратором:

private void createTestUser() {
Role adminRole = roleRepository.findByName("ROLE_ADMIN");
Role superUserRole = roleRepository.findByName("ROLE_SUPER_USER");
...
userJohn.setRoles(Arrays.asList(adminRole, superUserRole));
}

4. Зарегистрируйте обычных пользователей

Нам также нужно убедиться, что мы регистрируем обычных пользователей с помощью реализации registerNewUser() :

@Override
public void registerNewUser(String username, String email, String password) {
...
Role role = roleRepository.findByName("ROLE_USER");
user.setRoles(Arrays.asList(role));
}

Обратите внимание, что роли в системе следующие:

  1. ROLE_USER : для обычных пользователей (роль по умолчанию) — у них есть ограничение на количество сообщений, которые они могут запланировать в день.
  2. ROLE_SUPER_USER : нет ограничений по расписанию
  3. ROLE_ADMIN : дополнительные параметры администратора

5. Директор

Далее давайте интегрируем эти новые привилегии в нашу основную реализацию:

public class UserPrincipal implements UserDetails {
...

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (Role role : user.getRoles()) {
for (Privilege privilege : role.getPrivileges()) {
authorities.add(new SimpleGrantedAuthority(privilege.getName()));
}
}
return authorities;
}
}

6. Ограничьте запланированные публикации обычными пользователями

Давайте теперь воспользуемся преимуществами новых ролей и привилегий и ограничим обычных пользователей от планирования более, скажем, 3 новых статей в день , чтобы избежать спама на Reddit.

6.1. Почтовый репозиторий

Во-первых, мы добавим новую операцию в нашу реализацию PostRepository — для подсчета запланированных сообщений определенного пользователя в определенный период времени:

public interface PostRepository extends JpaRepository<Post, Long> {
...

Long countByUserAndSubmissionDateBetween(User user, Date start, Date end);

}

5.2. Запланированный пост-контроллер

Затем мы добавим простую проверку к методам schedule() и updatePost() :

public class ScheduledPostRestController {
private static final int LIMIT_SCHEDULED_POSTS_PER_DAY = 3;

public Post schedule(HttpServletRequest request,...) throws ParseException {
...
if (!checkIfCanSchedule(submissionDate, request)) {
throw new InvalidDateException("Scheduling Date exceeds daily limit");
}
...
}

private boolean checkIfCanSchedule(Date date, HttpServletRequest request) {
if (request.isUserInRole("POST_UNLIMITED_PRIVILEGE")) {
return true;
}
Date start = DateUtils.truncate(date, Calendar.DATE);
Date end = DateUtils.addDays(start, 1);
long count = postReopsitory.
countByUserAndSubmissionDateBetween(getCurrentUser(), start, end);
return count < LIMIT_SCHEDULED_POSTS_PER_DAY;
}
}

Здесь происходит пара интересных вещей. Во-первых, обратите внимание, как мы вручную взаимодействуем с Spring Security и проверяем, есть ли у вошедшего в данный момент пользователя привилегия или нет. Это не то, что вы делаете каждый день, но когда вам нужно это сделать, API очень полезен.

Согласно текущей логике — если у пользователя есть POST_UNLIMITED_PRIVILEGE — он может — неожиданно — планировать столько времени, сколько захочет.

Однако, если у них нет этой привилегии, они смогут ставить в очередь не более 3 сообщений в день.

7. Страница пользователей-администраторов

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

7.1. Показать всех пользователей

Во-первых, давайте создадим базовую страницу со списком всех пользователей в системе:

Здесь API для перечисления всех пользователей:

@PreAuthorize("hasRole('ADMIN_READ_PRIVILEGE')")
@RequestMapping(value="/admin/users", method = RequestMethod.GET)
@ResponseBody
public List<User> getUsersList() {
return service.getUsersList();
}

И реализация сервисного уровня:

@Transactional
public List<User> getUsersList() {
return userRepository.findAll();
}

Затем простой интерфейс:

<table>
<thead>
<tr>
<th>Username</th>
<th>Roles</th>
<th>Actions</th></tr>
</thead>
</table>

<script>
$(function(){
var userRoles="";
$.get("admin/users", function(data){
$.each(data, function( index, user ) {
userRoles = extractRolesName(user.roles);
$('.table').append('<tr><td>'+user.username+'</td><td>'+
userRoles+'</td><td><a href="#" onclick="showEditModal('+
user.id+',\''+userRoles+'\')">Modify User Roles</a></td></tr>');
});
});
});

function extractRolesName(roles){
var result ="";
$.each(roles, function( index, role ) {
result+= role.name+" ";
});
return result;
}
</script>

7.2. Изменить роль пользователя

Далее немного простой логики для управления ролями этих пользователей; начнем с контроллера:

@PreAuthorize("hasRole('USER_WRITE_PRIVILEGE')")
@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void modifyUserRoles(
@PathVariable("id") Long id,
@RequestParam(value = "roleIds") String roleIds) {
service.modifyUserRoles(id, roleIds);
}

@PreAuthorize("hasRole('USER_READ_PRIVILEGE')")
@RequestMapping(value = "/admin/roles", method = RequestMethod.GET)
@ResponseBody
public List<Role> getRolesList() {
return service.getRolesList();
}

И сервисный слой:

@Transactional
public List<Role> getRolesList() {
return roleRepository.findAll();
}
@Transactional
public void modifyUserRoles(Long userId, String ids) {
List<Long> roleIds = new ArrayList<Long>();
String[] arr = ids.split(",");
for (String str : arr) {
roleIds.add(Long.parseLong(str));
}
List<Role> roles = roleRepository.findAll(roleIds);
User user = userRepository.findOne(userId);
user.setRoles(roles);
userRepository.save(user);
}

Наконец — простой интерфейс:

<div id="myModal">
<h4 class="modal-title">Modify User Roles</h4>
<input type="hidden" name="id" id="userId"/>
<div id="allRoles"></div>
<button onclick="modifyUserRoles()">Save changes</button>
</div>

<script>
function showEditModal(userId, roleNames){
$("#userId").val(userId);
$.get("admin/roles", function(data){
$.each(data, function( index, role ) {
if(roleNames.indexOf(role.name) != -1){
$('#allRoles').append(
'<input type="checkbox" name="roleIds" value="'+role.id+'" checked/> '+role.name+'<br/>')
} else{
$('#allRoles').append(
'<input type="checkbox" name="roleIds" value="'+role.id+'" /> '+role.name+'<br/>')
}
});
$("#myModal").modal();
});
}

function modifyUserRoles(){
var roles = [];
$.each($("input[name='roleIds']:checked"), function(){
roles.push($(this).val());
});
if(roles.length == 0){
alert("Error, at least select one role");
return;
}

$.ajax({
url: "user/"+$("#userId").val()+"?roleIds="+roles.join(","),
type: 'PUT',
contentType:'application/json'
}).done(function() { window.location.href="users";
}).fail(function(error) { alert(error.responseText);
});
}
</script>

8. Конфигурация безопасности

Наконец, нам нужно изменить конфигурацию безопасности, чтобы перенаправить пользователей-администраторов на эту новую отдельную страницу в системе:

@Autowired 
private AuthenticationSuccessHandler successHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
...
.authorizeRequests()
.antMatchers("/adminHome","/users").hasAuthority("ADMIN_READ_PRIVILEGE")
...
.formLogin().successHandler(successHandler)
}

Мы используем собственный обработчик успеха аутентификации, чтобы решить, куда попадает пользователь после входа в систему :

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication auth)
throws IOException, ServletException {
Set<String> privieleges = AuthorityUtils.authorityListToSet(auth.getAuthorities());
if (privieleges.contains("ADMIN_READ_PRIVILEGE")) {
response.sendRedirect("adminHome");
} else {
response.sendRedirect("home");
}
}
}

И чрезвычайно простая домашняя страница администратора adminHome.html :

<html>
<body>
<h1>Welcome, <small><span sec:authentication="principal.username">Bob</span></small></h1>
<br/>
<a href="users">Display Users List</a>
</body>
</html>

9. Заключение

В этой новой части примера мы добавили в наше приложение несколько простых артефактов безопасности — роли и привилегии. Благодаря этой поддержке мы создали две простые функции — ограничение по расписанию для обычных пользователей и простое администрирование для пользователей с правами администратора.