IntelliJ+SpringBoot项目实战(十七)--在SpringBoot中整合SpringSecurity和JWT(下B)
八、SpringSecurity实现权限控制
在上节中介绍了SpringSecurity登录时从数据库中验证用户以及获取用户的权限集合。本文介绍如何进行权限控制。
在上节中,虽然实现了从数据库中获取用户并验证密码,但是还没有实现权限的控制,只是将用户的权限从v_user_auth或demo_user_auth中获取了而已。现在介绍如何实现权限的控制。
在进行权限开发之前,我们需要设计跟权限相关的数据库表,在项目开发中,权限的控制都需要设计用户表、资源表(权限ID+资源URL)、角色表、用户角色关系表、角色权限关系表,除此之外,还需要单位表、单位部门表、部门用户表,不过后面这3个表是跟业务有关。在OpenJweb低代码平台中,用户表为comm_user,角色表为comm_roles,资源表comm_auth,角色权限(资源)关系表comm_role_auth、用户角色关系表 comm_user_role。然后基于这些表再建一个v_user_auth方便用户权限的查询。
现在我们需要开发2个类,一个是MyFilterInvocationSecurityMetadataSource类,实现SpringSecurity的FilterInvocationSecurityMetadataSource接口,一个是MyAccessDecisionManager,实现AccessDecisionManager接口。下面先贴出实现的代码,然后再简单讲解一下,代码可能有点难度,大家掌握主要功能即可。在openjweb-sys工程实现下面2个类:
package org.openjweb.sys.auth.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
@Component
@Slf4j
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
//private static JdbcTemplate service = JdbcTemplateConfig.getDefaultJdbcTemplate();
@Resource(name="jdbcTemplateOne")
private JdbcTemplate service;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 从URL解析获取是否有对应的AUTH,并找出AUTH编码加到数组
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 首先获取菜单列表
List<Map<String, Object>> menuList = null;
try {
menuList = service.queryForList("select distinct auth_resource ,auth_id from comm_auth where "
+ "auth_resource is not null and auth_resource<>'#' and auth_resource like '/api%' ");// 权限列表
} catch (Exception ex) {
ex.printStackTrace();
}
//System.out.println("检查权限:"+requestUrl);
//if(requestUrl.equals("/")) {//对/控制权限吗????
// System.out.println("/权限返回......");
// return SecurityConfig.createList("ROLE_USER");//没有权限清单的,登录后可访问
//}
if(menuList==null||menuList.size()==0) {
log.info("权限清单为空......................");
}
for (Map<String, Object> map : menuList) {
log.info("遍历匹配权限.........");
String authResource = map.get("auth_resource").toString();
System.out.println(authResource+"比较"+requestUrl);
Long authId = new Long(map.get("auth_id").toString());
List<Map<String, Object>> roleList = null;
log.info("检查匹配:"+authResource+"----"+ requestUrl);
if (antPathMatcher.match(authResource, requestUrl)) {
try {
log.info("正在检查角色权限:,先查权限清单::::::::");
log.info(authResource);
roleList = service.queryForList(
"select a.auth_resource,a.comm_code from comm_auth a , comm_roles b,"
+ "comm_role_auth c where a.auth_id=? and a.auth_id=c.auth_id and c.role_id=b.role_id and a.auth_resource<>'#' and a.auth_resource is not null",
new Object[] { authId });
} catch (Exception ex) {
ex.printStackTrace();
}
String[] strArray = null;
if (roleList != null && roleList.size() > 0) {
strArray = new String[roleList.size()];
for (int i = 0; i < strArray.length; i++) {
strArray[i] = roleList.get(i).get("comm_code").toString();
}
log.info("返回权限数组。。。。。。。。。。。。。。。。。。。。");
return SecurityConfig.createList(strArray);
}
}
}
log.info("未在权限清单,返回。。。。。。");
return null;// SecurityConfig.createList("ROLE_LOGIN");//没有权限清单的,都可以访问 ,如登录访问用ROLE_USER
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
在上面的代码中,为了简单起见,没使用mybatis-plus查询数据库,直接使用JdbcTemplate进行查询。另外menuList=service.queryForList这句代码中的sql,是从openjweb项目的comm_auth权限表中获取的,下文会贴出此表的DDL。这个类的主要处理逻辑是:
(1)首先从数据库中获取到权限清单,或者叫资源清单(这里权限等同于资源,内容包括权限ID(auth_id),URL地址(auth_resource))。这一步的实现就是获取menuList。
(2)然后循环获得的menuList,从menuList逐条检查auth_resource中的URL是否匹配当前的请求URL地址,就是我们要访问的接口地址。如果检查到auth_resource是请求接口URL的一部分,那么说明这个请求接口受权限管控了,然后就根据角色表、权限表、角色权限关系表查询此权限对应有哪些角色,具体实现看roleList = service.queryForList这句的SQL。查到此权限对应的角色之后,然后将拥有此权限的所有角色,通过strArray[i] = roleList.get(i).get("comm_code").toString();这行代码加到权限数组里,然后通过SecurityConfig.createList(strArray)返回。
再做下总结,就是此类的作用是,根据请求的URL接口,查询数据库中哪些权限匹配这个接口,找到对应的权限并将权限编码加到数组,然后返回权限数组,这个权限数组返回是通过SecurityConfig.createList(strArray)返回,类型是SpringSecurity框架的ConfigAttribute。
接下来我们再开发MyAccessDecisionManager,见下面的代码:
package com.openjweb.sys.auth.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
@Slf4j
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) {
Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
for (ConfigAttribute configAttribute : ca) {
if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
&& auth instanceof UsernamePasswordAuthenticationToken) {
return;
}
if ("ROLE_USER".equals(configAttribute.getAttribute())
&& auth instanceof UsernamePasswordAuthenticationToken) {
return;
}
for (GrantedAuthority authority : auths) {
log.info("得到的权限-当前权限:" + authority.getAuthority() + "-----" + configAttribute.getAttribute());
if (configAttribute.getAttribute().equals(authority.getAuthority())) {
log.info("匹配:" + authority.getAuthority() + ",返回");
return;
}
}
}
log.info("权限无匹配,抛异常..........");
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
在上面的代码中,decide方法的ca参数就是上一个类SecurityConfig.createList(strArray)方法返回的结果,至于是怎么传递到这里,这个是SpringSecurity框架内部的处理流程,我们不需要去深究。这个类的decide方法的作用就是决策,就是将用户拥有的权限和访问这个资源所需要的权限进行循环比对,如果发现了有匹配的权限,就返回,允许访问资源,否则抛出权限不足的异常。如果我们在未登录的时候访问一个需要授权访问的URL,控制台会出现下面的日志:
得到的权限-当前权限:ROLE_ANONYMOUS-----userRole,这个就是上面代码中我们加的日志语句,可以看出,未登录时,SpringSecurity的 authority.getAuthority()返回的是匿名角色ROLE_ANONYMOUS,后面的configAttribute.getAttribute()返回的是userRole。userRole是第一个类运行后,查询到的访问请求链接要求的权限。
这2个类开发完成之后,需要在WebSecurityConfig.java中配置上,在此类中增加2个Bean:
@Bean
MyAccessDecisionManager cadm() {
//System.out.println("加载角色权限设置。。。。。。。。。。。。");
return new MyAccessDecisionManager();
}
@Bean
MyFilterInvocationSecurityMetadataSource cfisms() {
//System.out.println("加载权限设置。。。。。。。。。。。。");
return new MyFilterInvocationSecurityMetadataSource();
}
此类中,原来的http.cors()这段也换掉,替换为:
/* http.cors().and().csrf().disable()//登录表单
.formLogin()
.and()
.authorizeRequests()
.antMatchers(ALLOW_URL_LIST).permitAll()
.anyRequest().authenticated();
*/
//下面是第二阶段整合了数据库权限控制的示例
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(cfisms());
object.setAccessDecisionManager(cadm());
return object;
}
})
.and().formLogin().loginProcessingUrl("/login").permitAll()
.and()
.logout().permitAll().and().csrf().disable();
在上面的代码中,加载了我们上面讲的2个类。
接下来我们开发一个测试类,在openjweb-core工程创建一个测试接口类AuthDemoApi:
package org.openjweb.core.api;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/api/auth")
@RestController
public class AuthDemoApi {
/**
* 此接口的URL加到demo_user_auth中测试权限控制
* //测试地址:localhost:8001/api/auth/test
* @return
*/
@RequestMapping("test")
public String testauth(){
return "hello";//不做权限配置的话,返回hello
}
@RequestMapping("test2")
public String testauth2(){
return "hello2";//不做权限配置的话,返回hello
}
}
我们可以利用/api/auth/test和/api/auth/test2做测试。注意如果URL没收录到权限定义表里,则此权限只受白名单控制,不受权限控制。如果资源收录到权限表,但是没有分配给任何角色,则也是不受权限控制。当然也可以通过修改代码来改变这个规则。
然后我们还是创建下数据库表,包括角色表、权限表、角色权限关系表、用户角色关系表、以及针对这几个表,创建一个视图v_user_auth_demo:
CREATE TABLE `comm_roles` (
`role_id` bigint(20) NOT NULL,
`role_name` varchar(40) NOT NULL,
`cls_code` varchar(16) DEFAULT NULL,
`tree_code` varchar(100) DEFAULT NULL,
`p_tree_code` varchar(100) DEFAULT NULL,
`lvl_num` bigint(20) DEFAULT NULL,
`is_leaf` char(1) DEFAULT NULL,
`comm_code` varchar(16) DEFAULT NULL,
`node_name` varchar(80) DEFAULT NULL,
`node_desc` varchar(255) DEFAULT NULL,
`row_id` varchar(40) NOT NULL,
`create_dt` varchar(23) DEFAULT NULL,
`update_dt` varchar(23) DEFAULT NULL,
`create_uid` varchar(40) DEFAULT NULL,
`update_uid` varchar(40) DEFAULT NULL,
`sort_no` bigint(20) DEFAULT NULL,
`data_flg` varchar(5) DEFAULT NULL,
`sys_code` varchar(40) DEFAULT NULL,
PRIMARY KEY (`role_id`),
UNIQUE KEY `comm_code` (`comm_code`),
KEY `idx_1730778572184000913` (`comm_code`),
KEY `idx_1730778572184000914` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
INSERT INTO `comm_roles` VALUES ('1', '超级用户', 'COMM', null, null, null, null, 'adminRole', null, null, '1', '2009-05-06 10:57:04', '2013-10-16 20:49:48', 'admin', 'admin', null, 'Y', 'COMM');
INSERT INTO `comm_roles` VALUES ('2', '一般用户', 'COMM', null, null, null, null, 'userRole', null, null, '2', '2009-05-06 10:57:04', '2013-06-18 14:56:16', 'admin', 'admin', null, '1', null);
INSERT INTO `comm_roles` VALUES ('3', 'CRM管理员', 'CRM', null, null, null, null, 'crmAdminRole', null, null, '3', '2009-09-16 22:04:23', '2013-10-16 20:49:48', 'admin', 'admin', null, '1', null);
CREATE TABLE `comm_auth` (
`auth_id` bigint(20) NOT NULL,
`auth_name` varchar(60) NOT NULL,
`auth_type` varchar(16) DEFAULT NULL,
`auth_resource` varchar(255) NOT NULL,
`comm_code` varchar(30) NOT NULL,
`cls_code` varchar(10) NOT NULL,
`tree_code` varchar(100) DEFAULT NULL,
`p_tree_code` varchar(100) DEFAULT NULL,
`lvl_num` bigint(20) DEFAULT NULL,
`is_leaf` char(1) DEFAULT NULL,
`node_name` varchar(80) DEFAULT NULL,
`node_desc` varchar(255) DEFAULT NULL,
`row_id` varchar(40) NOT NULL,
`create_dt` varchar(23) DEFAULT NULL,
`update_dt` varchar(23) DEFAULT NULL,
`create_uid` varchar(40) DEFAULT NULL,
`update_uid` varchar(40) DEFAULT NULL,
`sort_no` bigint(20) NOT NULL,
`data_flg` varchar(10) NOT NULL,
`pic_file` varchar(120) DEFAULT NULL,
`sys_role` varchar(40) DEFAULT NULL,
`is_assign_to_com` varchar(10) DEFAULT NULL,
`sys_code` varchar(40) DEFAULT NULL,
`menu_url` varchar(200) NOT NULL,
`menu_sort_no` bigint(20) NOT NULL,
`new_url` varchar(255) DEFAULT NULL,
`is_layui` varchar(10) DEFAULT NULL,
`layui_name` varchar(80) DEFAULT NULL,
`layui_jump` varchar(255) DEFAULT NULL,
`is_vue` varchar(10) DEFAULT NULL,
`no_keep_alive` varchar(10) DEFAULT NULL,
`always_show` varchar(10) DEFAULT NULL,
`vue_path` varchar(400) DEFAULT NULL,
`vue_component` varchar(400) DEFAULT NULL,
`vue_redirect` varchar(60) DEFAULT NULL,
`vue_hidden` varchar(10) DEFAULT NULL,
`vue_icon` varchar(60) DEFAULT NULL,
PRIMARY KEY (`auth_id`),
UNIQUE KEY `comm_code` (`comm_code`),
UNIQUE KEY `tree_code` (`tree_code`),
KEY `idx_1730778570733000801` (`comm_code`),
KEY `idx_1730778570734000802` (`tree_code`),
KEY `idx_1730778570734000803` (`auth_name`),
KEY `idx_1730778570735000804` (`auth_id`,`data_flg`),
KEY `idx_1730778570735000805` (`is_vue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
INSERT INTO `comm_auth` VALUES ('2', '基本权限', null, '/api/auth/test', 'AUTH_BASIC', 'COMM', '002', null, null, null, '基本权限', null, 'fsdfddfagfdb', null, '2015-09-20 19:26:00', null, 'admin', '100', 'Y', null, null, null, null, '/login1.jsp', '100', null, null, null, null, null, null, null, null, null, null, null, null);
INSERT INTO `comm_auth` VALUES ('11', '测试权限', null, '/**/*.action,/**/*.do', 'AUTH_TEST', 'COMM', '001', null, null, null, '测试权限', null, '1', null, '2015-09-20 19:26:00', null, 'admin', '100', 'Y', null, null, null, null, '/**/*.action,/**/*.do', '100', null, null, null, null, null, null, null, null, null, null, null, null);
INSERT INTO `comm_auth` VALUES ('1084', '系统管理', null, '#', 'CRM_MENU01', 'CRM', '003', null, null, null, '系统管理', '系统管理/apps/images/ico_xtgl.gif fa-gears', '5df80291f8c44e48ab32342d904d5cd8', '2019-12-19 21:46:56', '2023-03-17 12:28:53', 'admin', 'system', '20', 'Y', 'layui-icon-set', null, 'Y', null, '#', '20', null, 'Y', 'SystemManagement', '3', 'Y', 'Y', 'Y', '/systemManagement', 'Layout', 'noRedirect', null, 'users-cog');
INSERT INTO `comm_auth` VALUES ('1181', '数据字典权限', null, '/**/test.jsp', 'AUTH_1179', 'COMM', '004', null, null, null, '数据字典权限', 'fdsfsd', '85991d1bac864ca6b7e88f73177bc8bc', null, '2015-09-20 19:26:00', null, 'admin', '100', 'Y', null, null, null, null, '/**/test.jsp', '100', null, null, null, null, null, null, null, null, null, null, null, null);
INSERT INTO `comm_auth` VALUES ('1607993', '信息图片数统计', null, '/comm/listCommReport!viewReport.action?repId=03', 'CRM_MENU0906', 'CRM', '021018', null, null, null, '信息图片数统计', null, '89a97e03998d4b00a296065d16100f76', null, '2015-09-20 19:26:00', null, 'admin', '100', 'Y', null, null, null, null, '/comm/listCommReport!viewReport.action?repId=03', '100', null, null, null, null, null, null, null, null, null, null, null, null);
INSERT INTO `comm_auth` VALUES ('20241128', 'SPRINGBOOT', null, '/api/auth/test2', 'CRM_MENU9911', 'CRM', '999999', null, null, null, '权限检测', '#', 'qwertyuiop1234567890olpokilopo11', '2024-11-28 09:01:00', '2024-11-28 09:01:00', 'system', 'system', '9999', 'Y', null, '1', 'N', 'OA', '/api/111111', '1111', null, 'N', null, null, 'N', 'N', 'N', null, null, null, null, null);
CREATE TABLE `comm_user_role` (
`serial_no` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
`role_id` bigint(20) DEFAULT NULL,
`create_dt` varchar(23) DEFAULT NULL,
`create_uid` varchar(40) DEFAULT NULL,
PRIMARY KEY (`serial_no`),
KEY `idx_1730778572600000953` (`user_id`),
KEY `idx_1730778572601000954` (`role_id`),
CONSTRAINT `FK_COMM_USER_ROLE1` FOREIGN KEY (`user_id`) REFERENCES `comm_user` (`user_id`) ON DELETE CASCADE,
CONSTRAINT `FK_COMM_USER_ROLE2` FOREIGN KEY (`role_id`) REFERENCES `comm_roles` (`role_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关系表';
INSERT INTO `comm_user_role` VALUES ('1667627586734003778', '1', '1', null, null);
INSERT INTO `comm_user_role` VALUES ('1667627586736003781', '1', '2', null, null);
INSERT INTO `comm_user_role` VALUES ('1667627586737003782', '1', '3', null, null);
CREATE TABLE `comm_role_auth` (
`serial_no` bigint(20) NOT NULL,
`auth_id` bigint(20) DEFAULT NULL,
`role_id` bigint(20) DEFAULT NULL,
`create_dt` varchar(23) DEFAULT NULL,
`create_uid` varchar(40) DEFAULT NULL,
PRIMARY KEY (`serial_no`),
UNIQUE KEY `role_id` (`role_id`,`auth_id`),
KEY `idx_1730778572192000915` (`role_id`,`auth_id`),
KEY `idx_1730778572193000916` (`auth_id`),
KEY `idx_1730778572193000917` (`role_id`),
CONSTRAINT `PK_COMM_ROLE_AUTH` FOREIGN KEY (`role_id`) REFERENCES `comm_roles` (`role_id`) ON DELETE CASCADE,
CONSTRAINT `PK_COMM_ROLE_AUTH2` FOREIGN KEY (`auth_id`) REFERENCES `comm_auth` (`auth_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限关系表';
INSERT INTO `comm_role_auth` VALUES ('1833', '11', '2', null, null);
INSERT INTO `comm_role_auth` VALUES ('1834', '2', '2', null, null);
INSERT INTO `comm_role_auth` VALUES ('20241128', '2', '2', null, null);
create view v_user_auth_demo as
select a.user_id,a.login_id,b.auth_id,b.comm_code,b.auth_name,b.auth_resource,b.pic_file,b.sort_no,b.menu_url,b.menu_sort_no,b.layui_name,b.layui_jump,b.is_layui
from comm_user a,comm_auth b , comm_user_role d,comm_role_auth e
where a.user_id = d.user_id
and a.is_in_use='Y'
and d.role_id = e.role_id
and e.auth_id = b.auth_id
and b.data_flg ='Y' ;
然后我们把CommUserMapp.java中selectAuthorities方法注解中的v_user_auth表改为v_user_auth_demo表。在comm_user表中,amdin用户的user_id是1,在面的SQL中,comm_roles表中,超级管理员的role_id是1,一般用户角色的ID是2,用户角色关系表comm_user_role中,admin用户有超管角色和一般用户2个角色。在权限表comm_auth中,auth_id为2映射的/api/auth/test,auth_id为20241128映射的URL是/api/auth/test2。在角色权限关系表comm_role_auth中,这2个权限都分配给了一般用户的角色。所以admin用户同时拥有这2个角色。
现在我们启动SpringBoot,分别访问:http://localhost:8001/api/auth/test和http://localhost:8001/api/auth/test2, 第一个访问的时候提示需要登录,输入用户名admin,密码Hello0214@,显示页面:
调用成功,控制台显示:
可看到找到一匹配项。http://localhost:8001/api/auth/test2 无论是否登录都可以访问,因为没有授权给任何角色,不过也可以修改代码,这个以后再说。 另外上节的白名单机制没生效是因为我们代码中http.authorizeRequests()的配置和前面不太一样了。以后在项目实际开发中,还需要完善白名单逻辑,比如允许访问静态资源。
接下来我们还需要演示下权限不足的情况,我们可以把用户admin拥有的访问/api/auth/test的权限删掉,因为此权限分配了了一般用户,所以把admin用户的一般用户角色删掉就可以了,执行下面的SQL:
delete from comm_user_role where user_id=1 and role_id=2 ;
权限删除后,可能因为缓存的原因,需要重启下SpringBoot才能使新权限生效,这个以后需要改造下。现在重启后再访问http://localhost:8001/api/auth/test,登录后会显示权限不足,说明权限控制生效:
如果我们再把删除的权限再加进来,就又能访问了。再执行
INSERT INTO `comm_user_role` VALUES ('1667627586736003781', '1', '2', null, null);
就可以了。
在这里顺便说一下,权限控制有的模式是在方法前面加权限注解,这个方式后面会介绍,本文介绍的是只要在数据库中把权限配置好就可以,不需要在程序代码中设置权限注解。在前后端分离项目中,前端也不需要关注权限编码,只需要调用接口就可以了。
到现在为止,我们已经实现了基于数据库用户、角色、权限等表实现了针对不同URL的权限控制。由于篇幅较长,下个帖子还要继续介绍JWT登录,所以本文就介绍到这里。
本文完整代码见Github: GitHub - openjweb/cloud at masterOpenJWeb is a java bases low code platform. Contribute to openjweb/cloud development by creating an account on GitHub.https://github.com/openjweb/cloud/tree/master