引子
最近在做一个小项目,涉及到权限相关。没有使用Shiro
框架和Spring Security
,是想自己控制权限。
- 权限表
- 角色表
- 权限-角色表
- 用户表
- 用户角色表
建表完成,然后自定义注解,使用拦截器,都是用烂的套路。
遇到的问题
有一个恶心的问题是,在需要权限的接口上,我要都使用注解,而且还要在注解的value
中标识需要什么权限。 所以一开始我是用枚举类,定义了一些权限code,然后数据库里也存了一份。
枚举类中的权限code主要是在注解里用。数据库中的用于配置角色的权限。
这样有一个问题,如果定义了新的接口,需要添加新的权限;那么我需要同时改数据库和枚举类。
忍不了…
解决办法
最好的方法就是在项目启动的时候,自动的检查有没有新的接口,如果有就自动的把权限code同步到数据库中。
所以权限code就要用现有的,且唯一的东西来标识。
那就是接口请求路径的url
开始动手
首先百度一下,毕竟我能想到的,肯定早就有人想到了。
果然,找到了一种方法:使用InitializingBean
- 实现
InitializingBean
接口,复写afterPropertiesSet
方法。 然后就会在初始化bean的时候执行该方法。
问题不大,因为要获取所有的请求路径,需要使用ApplicationContext
的getBeansWithAnnotation
方法。而我有一个工具类,可以随时获取ApplicationContext
对象。
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author lww
* @date 2019-03-27 11:20 AM
*/
@Component
public class SpringBeanFactoryUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeanFactoryUtils.context = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return context;
}
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
然后使用 ApplicationContext context = SpringBeanFactoryUtils.getApplicationContext();
获取ApplicationContext
对象,结果为null…
当然不是这个方法不行,是使用工具类取不到,但是直接注入是可以取到的。不过如果成功了,也不会有后面的事了。工具类取不到,是因为实现的是
ApplicationContextAware
接口。在SpringBoot
的启动流程中,InitializingBean
的afterPropertiesSet
在ApplicationContextAware
的setApplicationContext
之前。所以这时候是getApplicationContext
的结果是null。但是这时容器中是有ApplicationContext
的,所以使用Resource
或者Autowired
是可以注入的。可以去看一看SpringBoot
的启动流程。
第二种方法
在敲代码的时候看到一个类SpringApplicationRunListener
,点进去看看这个类,这是一个接口 看到Listener for the {@link SpringApplication} {@code run} method.
,一看就知道,就是这个类啦。
package org.springframework.boot;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Listener for the {@link SpringApplication} {@code run} method.
* {@link SpringApplicationRunListener}s are loaded via the {@link SpringFactoriesLoader}
* and should declare a public constructor that accepts a {@link SpringApplication}
* instance and a {@code String[]} of arguments. A new
* {@link SpringApplicationRunListener} instance will be created for each run.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @since 1.0.0
*/
public interface SpringApplicationRunListener {
/**
* 在run()方法开始执行时,该方法就立即被调用,可用于在初始化最早期时做一些工作
*/
default void starting() {
}
/**
* 当environment构建完成,ApplicationContext创建之前,该方法被调用
*/
default void environmentPrepared(ConfigurableEnvironment environment) {
}
/**
* 当ApplicationContext构建完成时,该方法被调用
*/
default void contextPrepared(ConfigurableApplicationContext context) {
}
/**
* 在ApplicationContext完成加载,但没有被刷新前,该方法被调用
*/
default void contextLoaded(ConfigurableApplicationContext context) {
}
/**
* 在ApplicationContext刷新并启动后,CommandLineRunners和ApplicationRunner未被调用前,该方法被调用
*/
default void started(ConfigurableApplicationContext context) {
}
/**
* 在run()方法执行完成前该方法被调用
*/
default void running(ConfigurableApplicationContext context) {
}
/**
* 当应用运行出错时该方法被调用
*/
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}
}
不过如果要使用监听器,需要先创建一个配置文件
- 在
resources
目录下创建一个文件夹,名称为META-INF
- 创建一个文件
spring.factories
- 最后配置启动的监听器
org.springframework.boot.SpringApplicationRunListener=你实现的监听器类
这里我的配置是 org.springframework.boot.SpringApplicationRunListener=com.ler.sparrowmanager.config.PermissionInitListener
在实现的监听器类,需要一个构造函数,这个是SpringBoot创建这个监听器的init方法,如果没有会报错
Exception in thread "main" java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.boot.SpringApplicationRunListener : com.ler.sparrowmanager.config.PermissionInitListener
at org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:445)
at org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:427)
at org.springframework.boot.SpringApplication.getRunListeners(SpringApplication.java:416)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:304)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
at com.ler.sparrowmanager.SparrowManagerApplication.main(SparrowManagerApplication.java:12)
Caused by: java.lang.NoSuchMethodException: com.ler.sparrowmanager.config.PermissionInitListener.<init>(org.springframework.boot.SpringApplication, [Ljava.lang.String;)
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:440)
... 6 more
public PermissionInitListener(SpringApplication application, String[] args) {
super();
}
复写started
方法就可以了,下面是完整的例子
package com.ler.sparrowmanager.config;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ler.sparrowmanager.domain.SmPermission;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 启动时 扫描所有url 添加到数据库
*
* @author lww
* @date 2020-04-11 09:27
*/
@Slf4j
public class PermissionInitListener implements SpringApplicationRunListener {
public PermissionInitListener(SpringApplication application, String[] args) {
super();
}
@Override
public void started(ConfigurableApplicationContext context) {
//获取application.properties配置中的属性 请求路径
ConfigurableEnvironment environment = context.getEnvironment();
String property = environment.getProperty("server.servlet.context-path");
if (StringUtils.isBlank(property)) {
property = "";
}
SmPermission permission;
//获取所有controller,因为我的Controller使用的都是RestController注解,如果使用的是Controller注解,要自己处理一下
Map<String, Object> beanMap = context.getBeansWithAnnotation(RestController.class);
for (Entry<String, Object> objectEntry : beanMap.entrySet()) {
Object value = objectEntry.getValue();
Class<?> valueClass = value.getClass();
//获取类上的 请求路径
RequestMapping mapping = valueClass.getAnnotation(RequestMapping.class);
String mid = "";
if (mapping != null) {
String[] value1 = mapping.value();
if (value1.length == 1) {
mid = value1[0];
}
}
Method[] methods = valueClass.getMethods();
for (Method method : methods) {
String rname = "";
String rcp = property;
permission = new SmPermission();
//获取方法上的url
RequestMapping rm = method.getAnnotation(RequestMapping.class);
GetMapping gm = method.getAnnotation(GetMapping.class);
PostMapping pm = method.getAnnotation(PostMapping.class);
PutMapping pum = method.getAnnotation(PutMapping.class);
DeleteMapping dm = method.getAnnotation(DeleteMapping.class);
if (rm != null && rm.value().length == 1) {
rcp = rcp + mid + rm.value()[0];
rname = rm.name();
} else if (gm != null && gm.value().length == 1) {
rcp = rcp + mid + gm.value()[0];
rname = gm.name();
} else if (pm != null && pm.value().length == 1) {
rcp = rcp + mid + pm.value()[0];
rname = pm.name();
} else if (pum != null && pum.value().length == 1) {
rcp = rcp + mid + pum.value()[0];
rname = pum.name();
} else if (dm != null && dm.value().length == 1) {
rcp = rcp + mid + dm.value()[0];
rname = dm.name();
} else {
continue;
}
permission.setPermsUrl(rcp);
//RequestMapping,GetMapping这些注解都有一个 name属性,可以用来存 权限名称
permission.setPermName(rname);
//检查是否已经存在,存在不插入
SmPermission selectOne = permission.selectOne(new QueryWrapper<SmPermission>().lambda()
.eq(SmPermission::getPermsUrl, permission.getPermsUrl()));
log.info("PermissionInitScanConfig_started_selectOne:{}", JSONObject.toJSONString(selectOne));
if (selectOne == null) {
//不存在插入,这里用了 Mybatis-Plus的 AR特性
boolean insertPermission = permission.insert();
log.info("PermissionInitScanConfig_started_insertPermission:{}", insertPermission);
}
}
}
}
}
结果:
启动时执行:
在数据库权限表中存储:
最后:
再使用拦截器,对请求的url和用户的角色对应的url进行比较来判断权限就可以了。
这样的好处就是,如果新增了接口,不需要额外配置,在启动的时候就会自动插入到数据库权限表中了。