1. Overview

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.

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

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

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

3. Setup

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

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

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

@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

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

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.

6.1. Post Repository

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

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

}

5.2. Scheduled Post Controller

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

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.

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

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

7. The Admin Users Page

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.

7.1. Display All Users

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

Here the API for listing out all users:

@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

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

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:

<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

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.


« 上一篇: Baeldung每周评论30
» 下一篇: Spring中如何重定向