虽然前面我们实现了通过数据库来配置用户与角色,但认证规则仍然是使用 HttpSecurity 进行配置,还是不够灵活,无法实现资源和角色之间的动态调整。这篇文章我们就介绍一下通过数据库查询某个URL资源的访问角色。
四、基于数据库的URL权限规则配置
1、数据库设计
这里在上一篇文章的基础上再添加 资源表和资源权限表 两种数据表,表结构如下所示:
- 资源表,保存每个菜单的URL
CREATE TABLE `menus` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -- 主键
`menu` varchar(20) NOT NULL, -- 菜单路径
`menu_name` varchar(30) NOT NULL, -- 菜单名称
`enabled` tinyint(1) DEFAULT '1', -- 是否启用 1-启用,0-未启用
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
- 资源角色表,主要保存 每个URL允许哪几个角色访问
CREATE TABLE `menu_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -- 主键
`m_id` bigint(20) unsigned DEFAULT NULL, -- 资源ID
`r_id` bigint(20) unsigned DEFAULT NULL, -- 角色ID
PRIMARY KEY (`id`),
UNIQUE KEY `m_r_idx` (`m_id`,`r_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
2、创建实体类及数据访问层
(1)、创建menus表的实体类 Menus
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Menus {
private Long id;
private String menu;
private String menuName;
private short enabled;
}
(2)、创建menus表的数据访问层 MenusMapper
@Repository
public interface MenusMapper {
@Select("select * from `menus`")
@Results(value = {
@Result(property = "id", column = "id"),
@Result(property = "menu", column = "menu"),
@Result(property = "menu_name", column = "menuName"),
@Result(property = "enabled", column = "enabled"),
@Result(property = "roles", column = "id",
many = @Many(select = "com.yuange.www.mapper.MenusMapper.queryRoleByMenuId"))
})
public List<Menus> queryAllMenus();
@Select("select * from `role` as r, menu_role as mr where r.id = mr.r_id and mr.m_id = #{id}")
@ResultType(Role.class)
List<Role> queryRoleByMenuId(Long id);
}
3、自定义 FilterInvocationSecurityMetadataSource
要实现动态配置权限,首先需要自定义 FilterInvocationSecurityMetadataSource:
注意:自定义 FilterInvocationSecurityMetadataSource 主要实现该接口中的 getAttributes 方法,该方法用来确定一个请求需要哪些角色。
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
// 创建一个AnipathMatcher,主要用来实现ant风格的URL匹配。
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Resource
private MenusMapper menusMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//从参数中获取请求URL
String url = ((FilterInvocation) object).getRequestUrl();
//查询所有的菜单需要的权限,实际项目中可以先加载到缓存中
List<Menus> menus = menusMapper.queryAllMenus();
Collection<String> roles = null;
try {
//匹配出符合条件的URL,并把URL所需要的权限返回
roles = menus.stream()
.filter(menu -> antPathMatcher.match(menu.getMenu(), url))
.findFirst().orElse(new Menus()).getRoles()
.stream().map(Role::getName).collect(Collectors.toList());
} catch (Exception e) {
System.err.printf("解析url[%s]需要的角色时出现异常: %s \n", url, e.getMessage());
}
//如果没有匹配到需要重新登录
if(CollectionUtils.isEmpty(roles)){
roles = Collections.singleton("ROLE_LOGIN");
}
return SecurityConfig.createList(roles.toArray(new String[]{}));
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
4、自定义 AccessDecisionManager
当一个请求走完 FilterInvocationSecurityMetadataSource 中的 getAttributes 方法后,接下来就会来到 AccessDecisionManager 类中进行角色信息的对比,自定义 AccessDecisionManager 代码如下:
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//获取当前登录用户的角色信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute ca: configAttributes){
//如果此时是 ROLE_LOGIN 角色并且用户登录操作,则直接放开
if("ROLE_LOGIN".equals(ca.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){
return;
}
for(GrantedAuthority ga: authorities){
//如用户角色中有匹配的角色则直接结束
if(ca.getAttribute().equals(ga.getAuthority())){
return;
}
}
}
//没有匹配的角色说明没有权限访问此资源
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
5、配置Security
这里与前文的配置相比,主要是修改了 configure(HttpSecurity http) 方法的实现并注入了两个 Bean。至此我们边实现了动态权限配置,权限和资源的关系可以在 menu_role 表中动态调整。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入userDetailsService
@Resource
private UserDetailsService userDetailsService;
@Resource
@Qualifier("MyAccessDecisionManager")
private AccessDecisionManager accessDecisionManager;
@Resource
@Qualifier("MySecurityMetadataSource")
private FilterInvocationSecurityMetadataSource metadataSource;
//配置内存用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService) //修改默认的 userDetailsService
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
// URL访问权限配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(metadataSource);
return object;
}
})
.and().formLogin().loginProcessingUrl("/login").permitAll()
.and().csrf().disable();
}
}
6、重启运行测试
(1)、用 admin 用户登录访问 “/add” 可以正常访问
(2)、用 user 用户登录访问 “/add”则出现如下错误