天天看點

從零開始,手打一個權限管理系統(第五章 權限控制)

作者:阿咕噜副業分享

前言

這章主要通過SpringSecurity來實作對權限的控制,權限粒度是到每個方法。

一、token驗證

第四章登入我們擷取到了token,每次請求的時候都必須驗證這個token是否合法、是否過期,是以我們需要一個攔截器來攔截每一次的請求;這裡我們可以通過繼承OncePerRequestFilter來實作我們對token的驗證;當然并不是所有請求都需要攔截,是以還需要一個白名單,來配置不需要被攔截的請求。
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "security.white")
public class PermitUrlProperties {

    @Getter
    @Setter
    private List<String> urls = new ArrayList<>();

}           
yml配置:
security:
  white:
    urls:
      - /login
      - /logout           
JwtAuthenticationTokenFilter
/**
 * token攔截驗證
 */
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JWTUtil jwtUtil;

    @Autowired
    private PermitUrlProperties permitUrlProperties;

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("JwtAuthenticationTokenFilter初始化...");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String requestUrl = httpServletRequest.getRequestURI();
        log.info("請求url:{}", requestUrl);
        // 白名單url放過
        if (filterWhiteUrl(requestUrl)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        String authToken = httpServletRequest.getHeader(SecurityConstants.AUTHORIZATION);
        if (StrUtil.isBlank(authToken)) {
            Result<String> result = Result.fail();
            result.setMsg("未登入");
            ResponseUtil.response(httpServletResponse, result);
            return;
        }
        boolean checkToken = jwtUtil.checkToken(authToken);
        if (checkToken) {
            Result<String> result = Result.fail();
            result.setMsg("會話已過期,請重新登入");
            httpServletResponse.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            ResponseUtil.response(httpServletResponse, result);
            return;
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //Context中的認證為空,進行token驗證
            Claims claims = jwtUtil.getClaimsFromToken(authToken);
            //從jwt中恢複使用者資訊和權限
            String id = claims.get(JWTUtil.ID, String.class);
            String orgId = claims.get(JWTUtil.ORGID, String.class);
            String username = claims.get(JWTUtil.USERNAME, String.class);
            String authorities = claims.get(JWTUtil.AUTHORITIES, String.class);
            List<String> list = JSON.parseObject(authorities, new TypeReference<List<String>>() {
            });
            JwtUser jwtUser = new JwtUser(id, orgId, username, "", AuthorityUtils.createAuthorityList(list.toArray(new String[0])));
            //如username不為空,并且能夠在資料庫中查到
            JwtAuthenticationToken jwtAuthenticationToken =
                    new JwtAuthenticationToken(jwtUser.getAuthorities(), jwtUser, null);
            //将authentication放入SecurityContextHolder中
            SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    /**
     * 過濾表名單的url
     *
     * @param url
     * @return
     */
    private boolean filterWhiteUrl(String url) {
        List<String> whiteList = permitUrlProperties.getUrls();
        if (CollectionUtil.isNotEmpty(whiteList)) {
            PathMatcher matcher = new AntPathMatcher();
            for (String releaseUrl : whiteList) {
                boolean match = matcher.match(releaseUrl, url);
                if (match) {
                    return true;
                }
            }
        }
        return false;
    }
}
           
更新下SpringSecurityConfigurer,将JwtAuthenticationTokenFilter加入配置中,部分代碼如下:
http.addFilterAfter(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);

  @Bean
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }
           
經過一系列的編譯調試後,啟動項目驗證:

1、擷取token

從零開始,手打一個權限管理系統(第五章 權限控制)

2、不帶token通路首頁

從零開始,手打一個權限管理系統(第五章 權限控制)

3、帶token通路首頁

從零開始,手打一個權限管理系統(第五章 權限控制)

4、token錯誤和過期通路首頁

從零開始,手打一個權限管理系統(第五章 權限控制)

二、權限驗證

1、開啟全局安全配置

在SpringSecurityConfigurer上加上@EnableGlobalMethodSecurity(prePostEnabled = true)就可以了;他會解鎖 @PreAuthorize 和 @PostAuthorize 兩個注解,@PreAuthorize 會在方法執行前進行驗證, @PostAuthorize 會在方法執行後進行驗證。

2、标記需要校驗的方法

我們在IndexController上面加上權限校驗,即@PreAuthorize("hasAuthority('sys:index')")

從零開始,手打一個權限管理系統(第五章 權限控制)

3、自定義未授權處理器

實作AccessDeniedHandler的handle接口即可

JwtAccessDeniedHandler
/**
 * 未授權通路處理
 */
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {

        Result<String> result = Result.fail(e.getMessage());
        httpServletResponse.setStatus(HttpStatus.HTTP_FORBIDDEN);
        ResponseUtil.response(httpServletResponse, result);
    }
}           

把這個加到SpringSecurityConfigurer裡面,新增代碼如下:

.exceptionHandling((execption) -> execption
                        // 未授權異常處理
                        .accessDeniedHandler(new JwtAccessDeniedHandler()));           
測試未授權
從零開始,手打一個權限管理系統(第五章 權限控制)

測試已授權

在JwtUserDetailsServiceImpl的權限清單中加入我們剛剛加的權限标記sys:index

從零開始,手打一個權限管理系統(第五章 權限控制)
重新登入,擷取新的token,并請求首頁,發現能夠正常通路
從零開始,手打一個權限管理系統(第五章 權限控制)

3、通過資料庫配置權限

前面都是寫死的權限,實際項目都是從資料庫中查詢的,這個項目我們采用RBAC 基于角色的通路控制,将所有權限都賦給角色,将角色賦給具體的使用者。

3.1、表設計

使用者表sys_user,用來存放使用者名、密碼等基礎資訊
CREATE TABLE `sys_user`  (
  `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '主鍵ID',
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '使用者名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '電話',
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '頭像',
  `org_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '機構ID',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '1' COMMENT '1-正常,0-鎖定',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '1' COMMENT '邏輯删除标記(1:顯示;0:删除)',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uk_username`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '使用者表' ROW_FORMAT = Dynamic;           
組織機構表sys_org
CREATE TABLE `sys_org`  (
  `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `sort` int NULL DEFAULT 1 COMMENT '排序',
  `type` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '機構類型',
  `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '機構編碼',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '機構名稱',
  `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '電話',
  `email` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '郵箱',
  `address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '位址',
  `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '備注',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '邏輯删除标記(1:顯示;0:删除)',
  `status` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '1:正常,0:鎖定',
	`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '機構管理' ROW_FORMAT = Dynamic;           
菜單表sys_menu,存放對應的菜單和權限辨別
CREATE TABLE `sys_menu`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜單ID',
  `title` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜單名稱',
  `permission` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權限辨別',
  `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父菜單ID',
  `sort` int NOT NULL DEFAULT 0 COMMENT '排序值',
  `type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜單類型 (0菜單 1按鈕)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '邏輯删除标記(1:顯示;0:删除)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜單權限表' ROW_FORMAT = Dynamic;           
角色表sys_role
CREATE TABLE `sys_role`  (
  `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '主鍵',
  `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '角色名',
  `role_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '角色編碼',
  `role_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '角色描述',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '1' COMMENT '邏輯删除标記(1:顯示;0:删除)',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `role_id_role_code`(`role_code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '系統角色表' ROW_FORMAT = Dynamic;           
角色菜單關系表sys_role_menu,一個角色擁有哪些菜單的權限
CREATE TABLE `sys_role_menu`  (
  `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色ID',
  `menu_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜單ID',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色菜單表' ROW_FORMAT = Dynamic;           
使用者角色表sys_user_role,一個使用者擁有哪些角色
CREATE TABLE `sys_user_role`  (
  `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '使用者ID',
  `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色ID',
	`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '使用者角色表' ROW_FORMAT = Dynamic;
           
使用者和角色是一對多的關系,角色和菜單也是一對多的關系
從零開始,手打一個權限管理系統(第五章 權限控制)

3.2、建立實體和實作CRUD

寫這些類其實是一個重複的工作,把這個項目寫完了,一定要做一個代碼生成器,一個一個地敲太費時費力了!!
從零開始,手打一個權限管理系統(第五章 權限控制)

4、測試驗證

4.1、初始化資料

之前我們開啟了權限驗證,現在初始化資料的時候先關一下;隻需要注釋掉SpringSecurityConfigurer上的@EnableGlobalMethodSecurity(prePostEnabled = true)這個注即可。

接口文檔:從零開始手打一個權限管理系統

初始化菜單資料
從零開始,手打一個權限管理系統(第五章 權限控制)
取消注釋,發送登入請求,可以發現權限資訊已經全部寫進去了,大家會發現新生成的token會比之前大很多,因為寫入了權限資訊,具體代碼可看JWTUtil的createToken方法
從零開始,手打一個權限管理系統(第五章 權限控制)
測試通路沒有權限的首頁
從零開始,手打一個權限管理系統(第五章 權限控制)
測試有權限的使用者新增
從零開始,手打一個權限管理系統(第五章 權限控制)
看看能不能登入
從零開始,手打一個權限管理系統(第五章 權限控制)

到這裡,這個系統的基本功能大部分都完成了,接下來我将繼續完善和優化細節!!!

目前版本:1.0.4

[代碼倉庫](https://gitee.com/ailot/study)

三、 體驗位址(http://test.ailot.vip)

背景資料庫隻給了部分權限,報錯屬于正常! 想學的老鐵給點點關注吧!!! 後期會開源前後端所有代碼!!!

我是阿咕噜,一個從網際網路慢慢上岸的程式員,如果喜歡我的文章,記得幫忙點個贊喲,謝謝!

繼續閱讀