手擼 Java Web RBAC 權限管理

前言

權限管理是在項目中經(jīng)常要使用到的模塊,有著極其重要的功能物遇。 在 Java 帝國中有兩個比較出名的權限框架乖仇,分別為 ShiroSpring Security,兩者各有優(yōu)缺询兴,但這不是本篇要討論的重點乃沙,這次我們不用任何權限框架來實現(xiàn) RBAC 權限管理。

本文所有代碼下載地址:https://github.com/zhaojun1998/Premission-Study

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());

}

image

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>

image

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());

}

image

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);

        }

    }

}

image

這里的為角色賦予權限同樣也是先刪除角色所擁有的權限,再添加表單提交的所有權限冲秽。

權限管理

添加權限

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";

}

image

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 的筆記中一一講到蘸秘。

本文所有代碼下載地址:https://github.com/zhaojun1998/Premission-Study

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子醋虏,更是在濱河造成了極大的恐慌寻咒,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颈嚼,死亡現(xiàn)場離奇詭異毛秘,居然都是意外死亡,警方通過查閱死者的電腦和手機粘舟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門熔脂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柑肴,你說我怎么就攤上這事霞揉。” “怎么了晰骑?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵适秩,是天一觀的道長。 經(jīng)常有香客問我硕舆,道長秽荞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任抚官,我火速辦了婚禮扬跋,結果婚禮上,老公的妹妹穿的比我還像新娘凌节。我一直安慰自己钦听,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布倍奢。 她就那樣靜靜地躺著朴上,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卒煞。 梳的紋絲不亂的頭發(fā)上痪宰,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音畔裕,去河邊找鬼衣撬。 笑死,一個胖子當著我的面吹牛扮饶,可吹牛的內(nèi)容都是我干的淮韭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼贴届,長吁一口氣:“原來是場噩夢啊……” “哼靠粪!你這毒婦竟也來了蜡吧?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤占键,失蹤者是張志新(化名)和其女友劉穎昔善,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體畔乙,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡君仆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牲距。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片返咱。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖牍鞠,靈堂內(nèi)的尸體忽然破棺而出咖摹,到底是詐尸還是另有隱情,我是刑警寧澤难述,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布萤晴,位于F島的核電站,受9級特大地震影響胁后,放射性物質發(fā)生泄漏店读。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一攀芯、第九天 我趴在偏房一處隱蔽的房頂上張望屯断。 院中可真熱鬧,春花似錦侣诺、人聲如沸殖演。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敏储,卻和暖如春阻星,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背已添。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工妥箕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人更舞。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓畦幢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缆蝉。 傳聞我的和親對象是個殘疾皇子宇葱,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 經(jīng)常用UITableView瘦真,一定會遇到數(shù)據(jù)為空的情況,這時需要在空頁面上放一個圖片和一行文字提示數(shù)據(jù)為空黍瞧,下面整...
    挨踢的蘋果閱讀 3,831評論 12 25
  • 前不久诸尽,我們村里的李伯伯去逝了,我被村里督管安排回去幫忙印颤。按照村里的約定您机,凡是村里有勞動力的人家,不管身處何處年局,只...
    伍海東閱讀 344評論 8 2
  • 近期在吳伯凡老師專欄中矢否,看到了關于對“白金&藍黑裙子”實驗的解釋仲闽,之前就有聽過這個話題,但當時沒有過多的在意兴喂。 而...
    芊沫閱讀 322評論 1 0