第五章 权限控制
前言
这章主要通过 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();
}
- 经过一系列的编译调试后,启动项目验证:
获取 token
不带 token 访问主页
带 token 访问主页
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 代码仓库
三、 体验地址
后台数据库只给了部分权限,报错属于正常!
想学的老铁给点点关注吧!!!
我是阿咕噜,一个从互联网慢慢上岸的程序员,如果喜欢我的文章,记得帮忙点个赞哟,谢谢!