1. 概述
在本篇中,我們將向我們的 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;
...
}請注意,用户角色關係是一種靈活的多對多關係。
接下來,我們將定義 角色 和 權限 實體。有關完整實施細節,請查看 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>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. 結論
在本案例研究的新部分中,我們添加了一些簡單的安全工件,包括角色和權限。有了這些支持,我們構建了兩個簡單的功能——標準用户的高級任務調度限制以及管理用户的基礎管理功能。