1. 概述
在本篇中,我们将为 Spring 构建的 Reddit 应用 引入基础的角色(Role)与权限(Privilege)机制,目标是实现一些实用功能,比如:限制普通用户每天最多只能发布 3 篇文章,防止刷屏。
同时,由于我们引入了管理员角色(Admin),自然也需要配套一个管理后台,方便管理员查看和调整用户权限。整个过程会结合 Spring Security 实现,但不会堆砌概念,而是聚焦实战。
2. User、Role 与 Privilege 实体设计
首先,我们需要对已有的 User
实体进行增强,加入角色支持:
@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;
...
}
✅ 关键点:
- 使用
@ManyToMany
实现用户与角色的多对多关系,灵活可扩展 fetch = FetchType.EAGER
确保登录时一次性加载角色,避免后续权限判断的 N+1 查询问题
接着定义 Role
和 Privilege
实体。权限是更细粒度的控制单元,角色则是一组权限的集合。具体实现可参考 Baeldung 上的权限模型文章,这里不再赘述。
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));
}
⚠️ 踩坑提醒:角色名建议统一以 ROLE_
开头,这是 Spring Security 的默认约定,避免后续 hasRole()
判断出错。
4. 普通用户注册时分配默认角色
新用户注册时,自动分配 ROLE_USER
角色:
@Override
public void registerNewUser(String username, String email, String password) {
...
Role role = roleRepository.findByName("ROLE_USER");
user.setRoles(Arrays.asList(role));
}
系统角色说明:
- ✅
ROLE_USER
:普通用户,每天最多发布 3 篇 - ✅
ROLE_SUPER_USER
:超级用户,无发布数量限制 - ✅
ROLE_ADMIN
:管理员,拥有后台管理权限
5. 将权限注入 Security Principal
为了让 Spring Security 能识别我们的权限体系,需要在 UserPrincipal
中重写 getAuthorities()
方法:
public class UserPrincipal implements UserDetails {
...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
for (Privilege privilege : role.getPrivileges()) {
authorities.add(new SimpleGrantedAuthority(privilege.getName()));
}
}
return authorities;
}
}
✅ 原理:Spring Security 的权限判断(如 hasRole()
、hasAuthority()
)依赖 GrantedAuthority
集合。我们将每个角色下的权限都转为 SimpleGrantedAuthority
,后续即可直接用权限名做判断。
6. 限制普通用户每日发布数量
现在可以实现核心功能:限制普通用户每日最多发布 3 篇。
6.1 PostRepository 添加统计接口
首先在 PostRepository
中添加按用户和时间范围统计的方法:
public interface PostRepository extends JpaRepository<Post, Long> {
...
Long countByUserAndSubmissionDateBetween(User user, Date start, Date end);
}
6.2 在 Controller 中添加限制逻辑
在 ScheduledPostRestController
中加入判断:
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 = postRepository.countByUserAndSubmissionDateBetween(getCurrentUser(), start, end);
return count < LIMIT_SCHEDULED_POSTS_PER_DAY;
}
}
⚠️ 关键细节:
- 使用
request.isUserInRole()
手动检查权限,虽然不常用,但在业务逻辑中非常实用 DateUtils.truncate()
将时间归零到当天 00:00,addDays
得到次日 00:00,构成完整一天区间- 只有拥有
POST_UNLIMITED_PRIVILEGE
权限的用户才能绕过限制
7. 管理员用户管理页面
有了角色体系,就可以为管理员提供简单的用户管理功能。
7.1 展示所有用户
提供一个接口获取所有用户列表:
@PreAuthorize("hasAuthority('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("hasAuthority('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("hasAuthority('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<>();
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. 安全配置:登录后跳转逻辑
为了让管理员登录后自动跳转到管理页,我们需要自定义 AuthenticationSuccessHandler
:
@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> privileges = AuthorityUtils.authorityListToSet(auth.getAuthorities());
if (privileges.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. 总结
本文通过引入角色与权限模型,为 Reddit 应用实现了两个实用功能:
- ✅ 发布频率控制:普通用户每日限发 3 篇,防止滥用
- ✅ 简易后台管理:管理员可查看和调整用户角色
整个实现简单粗暴但足够有效,适合中小型项目快速落地权限控制。关键在于 权限与角色分离设计 + Principal 权限注入 + 灵活的权限判断,这三板斧打下来,基础权限体系就稳了。