前言
權限管理是在項目中經(jīng)常要使用到的模塊,有著極其重要的功能物遇。 在 Java 帝國中有兩個比較出名的權限框架乖仇,分別為 Shiro 和 Spring Security,兩者各有優(yōu)缺询兴,但這不是本篇要討論的重點乃沙,這次我們不用任何權限框架來實現(xiàn) RBAC 權限管理。
RBAC 簡介
RBAC (Role-Based Access Control) 基于角色的權限訪問控制诗舰。
即用戶擁有角色,角色擁有權限崔涂。具體關于 RBAC 的好處我就不再贅言,如感興趣請自行查詢始衅。
數(shù)據(jù)庫設計
共有五張表冷蚂,分別為用戶表、角色表汛闸、權限表蝙茶、用戶-角色關系表、角色-權限關系表诸老。其中用戶表于角色表是多對多的關系隆夯,角色表于權限表也是多對多關系。具體每個字段的含義請查看相應的注釋。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權限名稱',
`description` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權限描述表',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '權限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權限名稱',
`description` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權限描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role_premission
-- ----------------------------
DROP TABLE IF EXISTS `role_premission`;
CREATE TABLE `role_premission` (
`role_id` int(11) NULL DEFAULT NULL,
`permission_id` int(11) NULL DEFAULT NULL,
INDEX `role_premission_uid_fk`(`role_id`) USING BTREE,
INDEX `role_premission_pid_fk`(`permission_id`) USING BTREE,
CONSTRAINT `role_premission_pid_fk` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `role_premission_uid_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用戶名',
`password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密碼',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用戶表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(11) NULL DEFAULT NULL,
`role_id` int(11) NULL DEFAULT NULL,
INDEX `user_role_uid_fk`(`user_id`) USING BTREE,
INDEX `user_role_rid_fk`(`role_id`) USING BTREE,
CONSTRAINT `user_role_rid_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `user_role_uid_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用戶角色表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
環(huán)境配置
本次基于的環(huán)境是 Spring + SpringMVC + MyBatis蹄衷,不過即使你不會這幾個框架也無所謂忧额,因為權限管理沒有涉及到太多這些框架的特性,用普通的 Servlet + JDBC 同樣也可以實現(xiàn)愧口。
因篇幅原因睦番,框架的配置文件我這里就不再貼出,但我會將源碼發(fā)到 Github耍属,你可以去下載本實例完整代碼托嚣。
實體類
首先需要創(chuàng)建三個與數(shù)據(jù)庫對應的實體類
public class User {
private Integer id;
private String username;
private String password;
// getter setter 略
}
public class Role {
private Integer id;
private String name;
private String description;
// getter setter 略
}
public class Permission {
private Integer id;
private String name;
private String description;
// getter setter 略
}
DAO 數(shù)據(jù)操作層
UserMapper
public interface UserMapper {
int insert(User record);
User selectByPrimaryKey(Integer id);
int updateByPrimaryKey(User record);
List<User> selectALL();
/**
* 查詢用戶擁有的角色列表
* @param id 用戶 id
* @return 角色列表
*/
List<Role> selectRolesByPrimaryKey(Integer id);
/**
* 刪除用戶所有角色
* @param id 用戶id
* @return 刪除成功的條數(shù)
*/
int deleteRoles(Integer id);
/**
* 為用戶賦予角色
* @param userId 用戶 id
* @param roleId 授予的角色 id
* @return 插入成功的條數(shù)
*/
int insertUserRole(@Param("user_id") Integer userId, @Param("role_id") Integer roleId);
/**
* 根據(jù)用戶名密碼查詢賬號是否存在
* @param username 用戶名
* @param password 密碼
* @return 查詢到的賬號
*/
User selectUserByUsernameAndPassword(@Param("username")String username,@Param("password")String password);
}
RoleMapper
public interface RoleMapper {
int insert(Role record);
Role selectByPrimaryKey(Integer id);
int updateByPrimaryKey(Role record);
List<Role> selectAll();
/**
* 查詢角色擁有的權限列表
* @param id 角色 id
* @return 權限列表
*/
List<Permission> selectPermissionsByPrimaryKey(Integer id);
/**
* 刪除角色所有的權限
* @param id 角色 id
* @return 刪除成功的條數(shù)
*/
int deletePermissions(Integer id);
/**
* 為角色添加一個權限
* @param roleId 角色 id
* @param permissionId 權限 id
* @return 插入成功的條數(shù)
*/
int insertRolePermission(@Param("role_id")Integer roleId, @Param("permission_id") Integer permissionId);
}
PermissionMapper
public interface PermissionMapper {
int insert(Permission record);
Permission selectByPrimaryKey(Integer id);
int updateByPrimaryKey(Permission record);
List<Permission> selectAll();
}
只需要一些簡單的 SQL 操作,如需要對應的 mapper.xml 可去 Github 查看厚骗。
用戶管理
用戶添加
HTML 頁面:
<form action="addUser" method="post">
用戶名:<input type="text" name="username">
密 碼:<input type="password" name="password">
<input type="submit" value="提交">
</form>
Controller:
public String addUserSubmit(User user) {
return userService.add(user) > 0 ? "success" : "error";
}
service 和 dao 略.
用戶登陸
HTML 頁面:
<form action="login" method="post">
用戶名 :<input type="text" name="username"><br>
密 碼:<input type="password" name="password"><br>
<input type="submit" value="登陸">
</form>
Controller:
public String login(String username, String password, HttpSession httpSession) {
User user = userService.selectUserByUsernameAndPassword(username, password);
if (user != null) {
httpSession.setAttribute("user", user);
return "登陸成功";
}
return "登陸失敗";
}
service 和 dao 略.
查看用戶列表
HTML 頁面:
<table border="1">
<tr>
<td>用戶名</td>
<td>密碼</td>
<td>操作</td>
</tr>
<c:forEach items="${users}" var="user">
<tr>
<td>${user.username}</td>
<td>${user.password}</td>
<td>
<a href="grantRoleView?id=${user.id}">賦予角色</a>
</td>
</tr>
</c:forEach>
</table>
Controller:
public ModelAndView listUser() {
return new ModelAndView("user.jsp").addObject("users", userService.getAllUser());
}
service 和 dao 略.
為用戶賦予角色
為用戶賦予角色需要先添加角色示启,請先看下面的添加角色后再來操作。
HTML 頁面:
<form action="grantRole">
<table border="1">
<tr>
<td>當前用戶</td>
<td>
${user.username}
<input type="hidden" name="id" value="${user.id}">
</td>
</tr>
<tr>
<td>已擁有角色</td>
<td>
<c:forEach items="${grantRole}" var="role">
<span>${role.name}</span>
</c:forEach>
</td>
</tr>
<tr>
<td>所有角色</td>
<td>
<c:forEach items="${roles}" var="role">
<input type="checkbox" name="roleId" value="${role.id}"> ${role.name}
</c:forEach>
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="提交"></td>
</tr>
</table>
</form>
Controller:
public String grantRole(int id, int[] roleId) {
userService.updateRoles(id, roleId);
return "success";
}
Service:
public void updateRoles(Integer id, int[] roleIds) {
userMapper.deleteRoles(id);
if (roleIds != null) {
for (int roleId : roleIds) {
userMapper.insertUserRole(id, roleId);
}
}
}
其實這里的修改授權角色只是將原來它擁有的所有角色刪除领舰,再分配給它提交的所有角色夫嗓。
角色管理
添加角色
HTML 頁面:
<form action="addRole" method="post">
角色名稱:<input type="text" name="name">
角色描述:<input type="text" name="description">
<input type="submit" value="提交">
</form>
Controller:
public String addRole(Role role) {
return roleService.add(role) > 0 ? "success" : "error";
}
service 和 dao 略.
角色列表
HTML 頁面:
<table border="1">
<tr>
<td>角色名稱</td>
<td>角色描述</td>
<td>操作</td>
</tr>
<c:forEach items="${roles}" var="role">
<tr>
<td>${role.name}</td>
<td>${role.description}</td>
<td>
<a href="grantPermissionView?id=${role.id}">賦予權限</a>
</td>
</tr>
</c:forEach>
</table>
Controller:
public ModelAndView listRole() {
return new ModelAndView("role.jsp").addObject("roles", roleService.getAll());
}
service 和 dao 略.
為角色賦予權限
HTML 頁面:
<form action="grantPermission">
<table border="1">
<tr>
<td>當前角色</td>
<td>
${role.name}
<input type="hidden" name="id" value="${role.id}">
</td>
</tr>
<tr>
<td>已擁有權限</td>
<td>
<c:forEach items="${grantPermission}" var="permission">
<span>${permission.name}</span>
</c:forEach>
</td>
</tr>
<tr>
<td>所有權限</td>
<td>
<c:forEach items="${permissions}" var="permission">
<input type="checkbox" name="premissionId" value="${permission.id}"> ${permission.name}
</c:forEach>
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="提交"></td>
</tr>
</table>
</form>
Controller:
public String grantPermission(int id, int[] premissionId) {
roleService.updatePermission(id, premissionId);
return "success";
}
Service:
public void updatePermission(Integer roleId, int[] permissionsIds) {
roleMapper.deletePermissions(roleId);
if (permissionsIds != null) {
for (int permissionId : permissionsIds) {
roleMapper.insertRolePermission(roleId, permissionId);
}
}
}
這里的為角色賦予權限同樣也是先刪除角色所擁有的權限,再添加表單提交的所有權限冲秽。
權限管理
添加權限
HTML 頁面:
<form action="addPermission" method="post">
權限名稱:<input type="text" name="name">
權限描述:<input type="text" name="description">
<input type="submit" value="提交">
</form>
Controller:
public String add(Permission permission) {
return permissionService.add(permission) > 0 ? "success" : "error";
}
service 和 dao 略.
權限列表
HTML 頁面:
<form action="addPermission" method="post">
權限名稱:<input type="text" name="name">
權限描述:<input type="text" name="description">
<input type="submit" value="提交">
</form>
Controller:
public String add(Permission permission) {
return permissionService.add(permission) > 0 ? "success" : "error";
}
service 和 dao 略.
權限攔截
既然已經(jīng)分配好用戶啤月,角色以及權限之間的關系了,那么我們就可以設置一些需要權限才能訪問的資源了劳跃。
我設置了 5 個 url, 并標注了需要何權限或何角色才可訪問:
/api/add # add 權限
/api/delete # delete 權限
/api/get # get 權限
/api/employee # employee 角色
/api/boss # boos 角色
我們可以用攔截器來攔截 /api/*
下的所有請求,那么如何區(qū)分不同請求分別需要什么權限呢浙垫?
這里我參考了 Shiro 的設計刨仑,即采用注解的方式,在相應的方法上用 @RequiredRole
和 @RequiredPremission
來標注相應的請求需要某個角色或某個權限才可訪問夹姥。
@RestController
@RequestMapping("/api")
public class APIController {
@RequiredPermission("add")
@RequestMapping("/add")
public String add() {
return "添加數(shù)據(jù)成功";
}
@RequiredPermission("delete")
@RequestMapping("/delete")
public String delete() {
return "刪除數(shù)據(jù)成功";
}
@RequiredPermission("get")
@RequestMapping("/get")
public String select() {
return "查詢數(shù)據(jù)成功";
}
@RequiredRole("boss")
@RequestMapping("/boss")
public String boss() {
return "此數(shù)據(jù)為 Boss 專用數(shù)據(jù), 你是 boss, 你可以查看";
}
@RequiredRole("employee")
@RequestMapping("/employee")
public String employee() {
return "此數(shù)據(jù)為員工專用數(shù)據(jù), 你是員工, 可以查看";
}
}
攔截器獲取攔截的方法上的注解即可得知需要什么權限杉武,以便來進行相應的判斷,Spring 攔截器:
public class PermissionHandlerInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
response.setHeader("Content-type", "text/html;charset=UTF-8");
Method method = ((HandlerMethod)handler).getMethod();
RequiredRole requiredRole = method.getAnnotation(RequiredRole.class);
RequiredPermission requiredPermission = method.getAnnotation(RequiredPermission.class);
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
response.getWriter().write("未登錄");
return false;
}
List<Role> userRoles = userService.getUserRoles(user.getId());
if (requiredRole != null) {
for (Role role : userRoles) {
if (role.getName().equals(requiredRole.value())) {
return true;
}
}
}
if (requiredPermission != null) {
for (Role role : userRoles) {
List<Permission> permissions = roleService.getPermissions(role.getId());
for (Permission persission : permissions) {
if (requiredPermission.value().equals(persission.getName())) {
return true;
}
}
}
}
response.getWriter().println("權限不足");
return false;
}
}
總結
基本實現(xiàn)就這些辙售,其實沒有很復雜的東西轻抱,只是將 RBAC 這個思想運用了起來。
那么反觀我們這個權限管理有什么缺陷呢旦部?
我來列舉幾點:
對密碼沒有進行加密處理, 應對密碼進行加鹽并散列祈搜。
每次請求都會去獲取所對應的權限數(shù)據(jù)和角色數(shù)據(jù),太耗費資源士八,應該進行緩存容燕。
不支持多憑證登陸,如可用郵箱也可用手機號登陸婚度。
這些問題我會在后續(xù)的 shiro 的筆記中一一講到蘸秘。