实现年结功能
SpringBoot整合Shiro实现动态过滤链
参看Shiro实现动态权限管理
Shiro动态过滤链加载前载入动态数据源
上一节说到自定义Runner 实现ApplicationRunner接口达成系统启动完成后初始化动态数据源
由于Shiro动态过滤链实在系统启动过程中加载的,所以我们在这里必须要保证动态数据源在Shiro之前加载
import cn.apcinfo.sys.pojo.AnnualInfo;
import cn.apcinfo.sys.service.AnnualService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class InitDynamicDataSourceListener {
private static final Logger log = LoggerFactory.getLogger(InitDynamicDataSourceListener.class);
@Autowired
private AnnualService annualService;
// 由于Spring中不允许存在相同名称的Bean,此处指定Bean的名称,防止因Bean名称冲突导致系统报错
@Bean(name = "initDynamicDataSourceSonListener")
public InitDynamicDataSourceListener initDynamicDataSourceListener() {
// 获取待注册数据源Bean的年度信息
List<AnnualInfo> annualInfoList = annualService.list();
// 注意: 可以在此处先注册已存在的数据源,由于系统内需要在年结操作完成后动态添加数据源,所以注册数据源的工作未放在此处处理
// // 注册各年度数据源
// for(AnnualInfo annualInfo:annualInfoList){
// ManualRegisteredDsBeanUtil.register((ConfigurableApplicationContext) applicationContext,annualInfo,properties);
// }
//
// // 验证Bean是否已注册成功
// Map beans = applicationContext.getBeansOfType(DruidDataSource.class);
// if(beans.size() != annualInfoList.size()+1){
// // 抛出系统运行错误,中止应用启动
// throw new Error("Application Start Failed: DataSource Bean Register Failed,Please Check DataSource Configuration is valid!");
// }
return new InitDynamicDataSourceListener();
}
}
/**
* Shiro 权限认证配置类
*/
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager,
AuthenticateService authenticateService,
AnnualService annualService,
// 只是为了保证Bean初始化的顺序
@Qualifier("initDynamicDataSourceSonListener") InitDynamicDataSourceListener initDynamicDataSourceListener) {
ShiroPermissionFilterFactoryBean shiroFilterFactoryBean = new ShiroPermissionFilterFactoryBean();
// 此处代码省略...
return shiroFilterFactoryBean;
}
}
/**
* Druid DataSource Bean 注册工具类
*/
public class ManualRegisteredDsBeanUtil {
/**
* 主动向Spring容器中注册bean
* @param applicationContext Spring容器
* @param annualInfo DataSource 年度信息
* @return 返回容器中DataSourceBean的名称及Bean的实例集合
*/
public static Map<String,DruidDataSource> register(
ConfigurableApplicationContext applicationContext,
AnnualInfo annualInfo,
DbConnProperties dbConnProperties) {
// 省略部分代码...
Map<String,DruidDataSource> result = new HashMap<>(1);
// 检查DataSource Bean是否存在,如果存在则直接推出
if(applicationContext.containsBean(dataSourceBeanName)) {
Object bean = applicationContext.getBean(dataSourceBeanName);
if (bean.getClass().isAssignableFrom(DruidDataSource.class)) {
result.put(dataSourceBeanName, (DruidDataSource) bean);
return result;
} else {
throw new RuntimeException("Bean 名称重复,该Bean不是DataSource Bean!");
}
}
// 省略部分代码...
result.put(dataSourceBeanName,druidDataSource);
return result;
}
}
public class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
/** 缓存 */
private String ANNUAL_INFO_CACHE_NAME = "annual_year";
private Cache<String, List<AnnualInfo>> annualInfoCache;
private DbConnProperties properties;
public DynamicDataSource(CacheManager cacheManager, DbConnProperties properties){
super();
this.annualInfoCache = cacheManager.getCache("annualInfoCache");
this.properties = properties;
}
/**
* Spring 上下文
*/
private ApplicationContext applicationContext;
/**
* 每一次执行SQL语句之前都会执行此方法,以确定本次访问数据库所对应的数据源的key
* 这里将数据源的bean name作为数据源的key 以方便后续调度数据数据源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
try {
List<AnnualInfo> annualInfoList = annualInfoCache.get(ANNUAL_INFO_CACHE_NAME);
// 如果年度有更新,则更新当前数据源
if(getTargetSource().size() != annualInfoList.size()){
Map<Object,Object> dataSources = new HashMap<Object,Object>();
for(AnnualInfo annualInfo:annualInfoList){
dataSources.putAll(ManualRegisteredDsBeanUtil.register((ConfigurableApplicationContext) applicationContext,annualInfo,properties));
}
super.setTargetDataSources(dataSources);
super.afterPropertiesSet();
}
} catch (Exception ex) {
// ignore
}
Object key = DataSourceScheduling.get();
// 如果没有指定数据源,则使用默认的数据源
if(key == null){
key = "baseDataSource";
}
return key;
}
// 部分代码省略...
}
由于Shiro只有在初次访问要求权限的资源时才会加载权限数据,此时无法获取到数据源调度类DataSourceScheduling存储的数据源Bean名称,导致选择了默认数据源,进而出现无法找到数据表的错误
@PostMapping("authenticate")
public ApcResult login(@ApiIgnore HttpServletResponse response,
@ApiIgnore HttpSession httpSession,
@NotNull(message = "帐号类型不允许为空!") Integer accountType,
@NotBlank(message = "用户名不允许为空!") String loginName,
@NotBlank(message = "密码不允许为空!") String password,
@NotBlank(message = "验证码不允许为空!") String verifyCode,
@NotNull(message = "年度不允许为空!") Integer apcAnnualYear) {
if (!new MathGenerator(1).verify((String) httpSession.getAttribute(VERIFY_CODE_ATTRIBUTE),verifyCode)) {
throw new ApcServiceException("验证码错误,请重新输入!");
}
Subject subject = SecurityUtils.getSubject();
ApcToken token = new ApcToken(loginName, password, accountType);
try {
subject.login(token);
// 这里将用户所选择的年度绑定到session中
subject.getSession().setAttribute("apcAnnualYear",apcAnnualYear);
userService.updateLastLoginTimeByLoginName(loginName);
return new ApcResult(userService.findCompleteUserByLoginName(ShiroUtil.getCurrentLoginUserName()));
} catch (ExcessiveAttemptsException e) {
Integer tryTimes = ((ApcExcessiveAttemptsException) e).getTryTime();
throw new ApcServiceException(StrUtil.format("登录失败,当前登录失败次数:{},达到5次账户将被锁定!", tryTimes));
} catch (LockedAccountException e) {
throw new ApcServiceException("登录失败,账户已锁定,请联系系统管理员解锁!");
} catch (Exception e) {
throw new ApcServiceException("登录失败:您输入的用户名或密码错误,请核对后重试!");
}
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 从session中获取年度参数并根据规则拼接生成javabean的名称设置到数据源调度类
Integer year = (Integer) SecurityUtils.getSubject().getSession().getAttribute("apcAnnualYear");
DataSourceScheduling.set("dj" + year + "DataSource");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<String> menuCodes = authenticateService.listFunctionCodeByLoginName(principalCollection.getPrimaryPrincipal().toString());
info.addStringPermissions(menuCodes);
return info;
}
// 清除缓存,重新加载数据源
// 注意,这里不能调用cache.clear()方法清空缓存,clear()只是清空缓存数据集合而不是将缓存数据清空
annualInfoCache.put(ANNUAL_INFO_CACHE_NAME,null);
this.list();
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_URL,
timeout: 10000, // 超时时长,单位: ms
// 跨域携带cookie配置
withCredentials: true
})
service.interceptors.request.use(
config => {
config.headers['X-Requested-With'] = 'XMLHttpRequest';
if(config.method === 'get'){
config.params = disposeRequestParam(config.params);
} else if(config.method === 'post'){
config.data = disposeRequestParam(config.data);
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
/**
* 注意:参数可能未被qs序列化(后端使用@RequestBody接收参数的情况),
* 也有可能已被qs序列化,所以此处要根据实际拿到的参数类型分类处理
*/
function disposeRequestParam(params){
// 当前无任何参数,将参数修改为空json
if(params === null || params === undefined){
params = {};
}
// 参数是否为字符串
let paramsIsString = false;
// 如果参数已被序列化,将其转为json对象
if(typeof params === 'string'){
paramsIsString = true;
params = qs.parse(params);
}
params = Object.assign(params,{
apcAnnualYear: store.state.certified.year
})
if(paramsIsString === true){
return qs.stringify(params)
} else {
return params;
}
}
另外,后端在获取年度参数时需要做特殊处理,可参看SpringBoot获取请求体中的数据
多数据库mybatis selectKey报错的解决方案
由于系统多数据源产生多个类似数据库时,mysql auto_increment查询报错
问题原因:
SELECT auto_increment FROM information_schema.tables where table_name="t1"
如上SQL表示从Mysql的所有数据库中查询t1表的auto_increment值,如果多个库中存在名称相同的表会导致查询出多个结果
而此时配合Mybatis selectKey使用时,会出现期望结果1,而实际结果多个的错误,如下:
<selectKey resultType="integer" keyColumn="pk_id" keyProperty="id" order="BEFORE">
SELECT auto_increment FROM information_schema.tables where table_name="t1"
selectKey>
解决方案: 在查询auto_increment时指定数据库名称,sql如下:
SELECT auto_increment FROM information_schema.tables where table_name="t1" AND TABLE_SCHEMA = 'test'
本次项目中,由于动态数据源的原因,此处无法定向指定数据库名称,采用如下迂回方案:
public class AnnualYearVO {
// 注意: 前端每一次请求都会传入参数
private Integer apcAnnualYear;
public Integer getApcAnnualYear() {
return apcAnnualYear;
}
public void setApcAnnualYear(Integer apcAnnualYear) {
this.apcAnnualYear = apcAnnualYear;
}
}
<selectKey resultType="integer" keyColumn="pk_id" keyProperty="id" order="BEFORE">
<bind name="apcAnnualYear" value="'%'+apcAnnualYear+'%'" />
SELECT auto_increment FROM information_schema.tables where table_name="t1" AND TABLE_SCHEMA LIKE #{apcAnnualYear}
selectKey>
注意,如上解决方案只能作为临时解决方案,弊端如下:
感谢以下博客提供的解决方案:
Shiro实现动态权限管理
SpringBoot获取请求体中的数据