1. 概述
在本篇中,我們將向我們的 Reddit 應用引入簡單的角色和權限,以便能夠執行一些有趣的操作,例如限制普通用户每天可以安排的帖子數量。
由於我們將擁有一個 Admin 角色——以及隱含的 admin 用户——我們還將添加一個 admin 管理區域。
2. User, Role 和 Privilege 實體
首先,我們將修改 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;
...
}
注意 User-Role 關係是靈活的多對多關係。
接下來,我們將定義 Role 和 Privilege 實體。有關該實現的詳細信息,請查看 Baeldung 上的這篇文章。
3. 安裝配置
接下來,我們將對項目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));
}
並將我們的測試用户設置為管理員:
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));
}
請注意,系統中的角色如下:
- ROLE_USER: 用於普通用户(默認角色)– 這些用户每天可以安排的帖子數量有限
- ROLE_SUPER_USER: 無排程限制
- 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>用户名</th> <th>角色</th> <th>操作</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+'\')">修改用户角色</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">修改用户角色</h4> <input type="hidden" name="id" id="userId"/> <div id="allRoles"></div> <button onclick="modifyUserRoles()">保存更改</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>歡迎,<small><span sec:authentication="principal.username">Bob</span></small></h1> <br/> <a href="users">顯示用户列表</a> </body> </html>9. 結論
在本案例研究的新部分中,我們添加了一些簡單的安全工件到我們的應用程序中——角色和權限。有了這些支持,我們構建了兩個簡單的功能——標準用户的時間安排限制以及管理用户的基礎管理功能。