从零开始,手打一个权限管理系统(第五章权限控制)
前言
这章主要通过SpringSecurity来实现对权限的控制,权限粒度是到每个方法。 一、token验证
第四章登录我们获取到了token,每次请求的时候都必须验证这个token是否合法、是否过期,所以我们需要一个拦截器来拦截每一次的请求;这里我们可以通过继承OncePerRequestFilter来实现我们对token的验证;当然并不是所有请求都需要拦截,所以还需要一个白名单,来配置不需要被拦截的请求。@Slf4j @Configuration @ConfigurationProperties(prefix = "security.white") public class PermitUrlProperties { @Getter @Setter private List 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 result = Result.fail(); result.setMsg("未登录"); ResponseUtil.response(httpServletResponse, result); return; } boolean checkToken = jwtUtil.checkToken(authToken); if (checkToken) { Result 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 list = JSON.parseObject(authorities, new TypeReference>() { }); 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 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 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)
后台数据库只给了部分权限,报错属于正常! 想学的老铁给点点关注吧!!! 后期会开源前后端所有代码!!!
我是阿咕噜,一个从互联网慢慢上岸的程序员,如果喜欢我的文章,记得帮忙点个赞哟,谢谢!