Adding Roles and Privileges To the Reddit App – 为Reddit应用程序添加角色和权限

最后修改: 2015年 7月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this installment, we’ll introduce simple roles and privileges into our Reddit app, to then be able to do some interesting things such as – limit how many posts a normal user can schedule to Reddit daily.

在这一期中,我们将在我们的Reddit应用中引入简单的角色和权限,然后能够做一些有趣的事情,例如–限制一个普通用户每天可以安排多少个帖子到Reddit。

And since we’re going to have an Admin role – and implicitly an admin user – we’re also going to add an admin management area as well.

由于我们将有一个管理员角色–以及隐含的一个管理员用户–我们也将添加一个管理员管理区。

2. User, Role and Privilege Entities

2.用户角色权限实体

First, we will modify the User entity – that we use it through our Reddit App series – to add roles:

首先,我们将修改User实体–我们通过我们的Reddit App系列使用它来添加角色。

@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;

    ...
}

Note how the User-Role relationship is a flexible many to many.

请注意,用户-角色关系是一种灵活的多对多关系。

Next, we’re going to define the Role and the Privilege entities. For the full details of that implementation, check out this article on Baeldung.

接下来,我们将定义RolePrivilege实体。关于该实现的全部细节,请查看Baeldung的这篇文章

3. Setup

3.设置

Next, we’re going to run some basic setup on project bootstrap, to create these roles and privileges:

接下来,我们要在项目bootstrap上运行一些基本设置,以创建这些角色和权限。

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));
}

And make our test user an admin:

并使我们的测试用户成为管理员。

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

4. Register Standard Users

4.注册标准用户

We’ll also need to make sure that we’re registering standard users via the registerNewUser() implementation:

我们还需要确保我们通过registerNewUser()实现来注册标准用户。

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

Note that the Roles in the system are:

请注意,系统中的角色是。

  1. ROLE_USER: for regular users (the default role) – these have a limit on how many posts they can schedule a day
  2. ROLE_SUPER_USER: no scheduling limit
  3. ROLE_ADMIN: additional admin options

5. The Principal

5.校长

Next, let’s integrate these new privileges into our principal implementation:

接下来,让我们把这些新的权限整合到我们的主要实现中。

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. Restrict Scheduled Posts by Standard Users

6.限制标准用户的预定帖子

Let’s now take advantage of the new roles and privileges and restrict standard users from scheduling more than – say – 3 new articles a day – to avoid spamming Reddit.

现在让我们利用新的角色和权限,限制标准用户每天安排超过–比如–3篇新文章–以避免向Reddit发送垃圾邮件。

6.1. Post Repository

6.1.邮政储存库</strong

First, we’ll add a new operation to our PostRepository implementation – to count the scheduled posts by a specific user in specific time period:

首先,我们将在PostRepository实现中添加一个新的操作–统计特定用户在特定时间段内的预定帖子。

public interface PostRepository extends JpaRepository<Post, Long> {
    ...
    
    Long countByUserAndSubmissionDateBetween(User user, Date start, Date end);

}

5.2. Scheduled Post Controller

5.2.预定的帖子控制器

Then, we will add a simple check to both schedule() and updatePost() methods:

然后,我们将为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;
    }
}

There are a couple interesting things going on here. First – notice how we’re manually interacting with Spring Security and checking if the currently logged in user has a privilege or not. That’s not something you do every day – but when you do have to do it, the API is very useful.

这里发生了几件有趣的事情。首先,注意到我们是如何与Spring Security进行手动交互,并检查当前登录的用户是否有特权的。这不是你每天都要做的事情–但当你不得不这样做的时候,API是非常有用的。

As the logic currently stands – if a user has the POST_UNLIMITED_PRIVILEGE – they’re able to – surprise – schedule however much they choose to.

按照目前的逻辑–如果一个用户拥有POST_UNLIMITED_PRIVILEGE–他们就能–惊喜地–安排任何他们选择的数量。

If however, they don’t have that privilege, they’ll be able to queue up a max of 3 posts per day.

然而,如果他们没有这种特权,他们每天最多只能排队发3个帖子。

7. The Admin Users Page

7.管理用户页面

Next – now that we have a clear separate of users, based on the role they have – let’s implement some very simple user management for the admin of our small Reddit app.

接下来–现在我们有了一个清晰的用户分类,基于他们所拥有的角色–让我们为我们的小型Reddit应用程序的管理员实现一些非常简单的用户管理。

7.1. Display All Users

7.1.显示所有用户

First, let’s create a basic page listing all the users in the system:

首先,让我们创建一个基本页面,列出系统中的所有用户。

Here the API for listing out all users:

这里是列出所有用户的API。

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

And the service layer implementation:

以及服务层的实现。

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

Then, the simple front-end:

然后,简单的前端。

<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. Modify User’s Role

7.2.修改用户的角色

Next, some simple logic to manage the roles of these users; let’s start with the controller:

接下来,一些简单的逻辑来管理这些用户的角色;让我们从控制器开始。

@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();
}

And the service layer:

还有服务层。

@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);
}

Finally – the simple front-end:

最后–简单的前端。

<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. Security Configuration

8.安全配置

Finally, we need to modify the security configuration to redirect the admin users to this new, separate page in the system:

最后,我们需要修改安全配置,将管理员用户重定向到系统中这个新的、独立的页面。

@Autowired 
private AuthenticationSuccessHandler successHandler;

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

We’re using a custom authentication success handler to decide where the user lands after login:

我们正在使用一个自定义的认证成功处理程序来决定用户在登录后的位置

@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");
        }
    }
}

And the extremely simple admin homepage adminHome.html:

还有极其简单的管理主页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. Conclusion

9.结论

In this new part of the case study, we added some simple security artifacts into our app – roles and privileges. With that support, we built two simple features – a scheduling limit for standard users and a bare-bones admin for admin users.

在案例研究的这个新部分,我们在我们的应用程序中添加了一些简单的安全工件–角色和权限。有了这种支持,我们建立了两个简单的功能–为标准用户提供调度限制,为管理员用户提供裸露的管理。