开发背景:
用户提交表单后,插入到对应数据库表的字段中去,因需要保存是哪一个用户提交的,所以需要拿到主表的user_id,更新功能为记录提交时间,短时间不得再次提交
在对一个已有角色权限分配,登录进行基础业务开发的时候,在原有用户表的基础上添加了一个字段来记录时间,前端提交表单后,在后端多表联查时仅需要主键和这个字段的值(并未写sql),调用了MP层的save(密码重置的问题)方法,后续又调用了Update方法(解决角色权限问题,乌龙,因为当时未注意调用的是UserService层写的同名方法,在这耽误了不少的时间)
业务开发思路
这一业务需求并不困难,基本思路为:用户提交表单 --> 先拿到登录信息的user_id --> 根据user_id查找到主表用户的新字段
-->如果字段为空,则保存本地时间 -->保存user_id插入表单信息到对应的表中
-->如果字段不为空,则和当前时间作比较
--> 当前时间小于字段时间,则提示用户处于冻结时间无法操作 --> 并抛出异常
--> 当前时间大于字段时间,则重置冻结时间为当前时间+5分钟 --> 保存user_id插入表单信息到对应的表中
设计到的主要方法以及技术:
MybatisPlus:
Service层:
getOne()方法:
用于查询符合条件的一条记录,如果有多条仅返回第一条数据
T为实体类类型,Wrapper<T> 是 MyBatis-Plus 提供的条件构造器,用于构建SQL查询条件,第二个提供了是否抛出异常的选项,如果设置为True则结果空是抛出异常,否则忽略
eq方法()
用于构建等值条件查询
- 等于 =
column是查询的字段名 val 表示要查询的字段值
default Children eq(R column, Object val) {
return this.eq(true, column, val);
}
Children eq(boolean condition, R column, Object val);
select()方法:
指定需要查询哪些字段。columns:一个可变长度参数列表,类型为String,表示数据库表中的列名。可以传入一至多个列名,这些列名将会被包含在最终生成的SQL查询语句的SELECT子句中
QueryWrapper<T> select(String... columns);
.last()方法
last方法在SQL语句的最后添加自定义的SQL语句或SQL片段,在调用getOne或者selectOne时可以先保证不会报错
- 无视优化规则直接拼接到 sql 的最后
注意事项:只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
- 例:
last("limit 1")
last(String lastSql)
last(boolean condition, String lastSql)
.set()方法
对某个字段set值
- 例:
set("name", "老李头")
- 例:
set("name", "")
--->数据库字段值变为空字符串 - 例:
set("name", null)
--->数据库字段值变为null
set(String column, Object val)
set(boolean condition, String column, Object val)
save()方法
根据实体类对象插入数据
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
update()方法
需要创建构造器,如果想传实体类调UpdateByid就可以
// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
业务实际开发过程
1.主表添加冻结字段记录冻结时间
2.用户提交表单后,调用接口
2.1 先拿到登录信息的user_id(光拿user_id或拿Userinfo(包含user_id))
当时考虑的是后续说不定会用到Userinfo里面的字段,所以选择的第二种
2.1.1 只拿user_id
Controller层继承了一个AbstractController(系统自写)
在AbstractController中用到的方法
protected SysUserEntity getUser() {
return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
}
protected Long getUserId() {
return getUser().getUserId();
}
代码在第二种方式有解释,因为是声明为protected,所以咱们可以直接在Controller层调用
2.1.2 拿Userinfo(包含user_id)
Controller层
创建VO对象调用service层getLoginUserInfo方法
UserInfoVO userInfoVO = sysUserService.getLoginUserInfo();
在service层中的实现:
创建实体对象: SecurityUtils是Shiro框架提供的一个工具类,.getSubject()方法返回的是线程绑定的Subject实例,包含了用户的大部分信息(用户名,密码,授权,状态等等),.getPrincipal()是获取主体的主要身份信息,然后将返回的Subjcet对象强转为SysUserEntity对象
判断登录用户是否为空:调用了Spring框架中的Asset进行条件判断。它确保了从安全上下文中获取到的当前登录用户(loginUser)不为空。如果为空,则抛出一个带有自定义错误消息
创建VO层对象接收基本信息: 调用Mapper的getUserInfoById(),在dao层写的多表联查sql语句
填充用户对应的角色信息,因为在user表中并没有直接绑定用户的role,而是通过关联表查询,但这个也不是拿到对应的role_id,而是名称,返回VO对象
public UserInfoVO getLoginUserInfo() {
SysUserEntity loginUser = (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
Assert.notNull(loginUser,"获取当前登录人员信息失败!");
UserInfoVO userInfoVO = this.baseMapper.getUserInfoById(loginUser.getUserId());
// 获取用户对应的角色信息
userInfoVO.setRoleNames(sysUserRoleService.queryRoleNameList(loginUser.getUserId().toString()));
return userInfoVO;
}
2.2 根据User_id查找冻结时间并判断 问题代码(密码失效问题)(权限失效问题)
密码失效
最开始的方法比较草率,导致出了问题,根据user_id先拿到user对象的所有信息,然后set时间,然后调用方法直接保存
SysUserEntity user = sysUserService.getById(getUserId());
long nowTimestamp = new Date().getTime();
long freezeTimestamp = nowTimestamp + 5 * 60 * 1000;
Date freezeUntilDate = new Date(freezeTimestamp);
user.setFreezeUntil(freezeUntilDate);
System.out.println(user);
sysUserService.updateByid(user);
问题出在拿到的user对象将所有的数据取了出来,然而密码是加密的形式,再进行保存便出现了原始密码并不可用的问题,而且其他数据也并不需要,拿整个对象的数据就多此一举了
权限失效
sysUserService.update(user);
其实问题也是出在了这里,当时将密码重置的问题解决后,在第二天发现权限消失了,又回来看这个代码,当时因为在User的实体类中定义了roleIdList的list集合,但是在数据库主表中的并未设置这个字段,而且注解设置的也是false不存在,然后在后面修正的代码测试时拿到的数据也是为空,我就在想会不会是其他的地方用到了这个字段,例如登录的时候对其进行了操作?
然后为了验证是不是这一个方法出了问题,有且仅有可能是这里出了问题,所以又用Warpper去写了一个方法尝试,问题便解决了,但还是想知道为什么会有什么问题
UpdateWrapper<SysUserEntity> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("user_id", userInfoVO.getUserId())
.set("freeze_until", newFreezeUntilDate)
sysUserService.update(user, updateWrapper);
MabatisPlus根据后面的代码也不会对空的字段进行操作,然后就去翻了翻对权限分配的一些Service层的方法,结果发现在Service层有个同名的Update方法,在Controller层调用的也是这一层的方法
@TableField(exist=false)
private List<Long> roleIdList;
Service层的实现
这是在User修改个人信息的时候调用到的方法
检查密码:对传入的对象中的密码字段进行校验,如果为空,则将密码设为null,这样在更新时不会修改用户的密码。如果不为空,则使用Sha256Hash进行加密(结合了用户盐值user.getSalt()),并将加密后的密码字符串(十六进制形式)重新赋给用户对象的密码字段。 Sha256Hash也是Shiro下的方法
更新用户的基本信息:调用mabtisplus的updateByid方法,根据实体类非空属性更新对应id的数据
因为在修改用户的界面可以修改用户的角色属性,即更改权限,所以在调用saveOrUpdate方法时,后端传回RoleIdList是null值,
public void update(SysUserEntity user) {
if(StringUtils.isBlank(user.getPassword())){
user.setPassword(null);
}else{
user.setPassword(new Sha256Hash(user.getPassword(), user.getSalt()).toHex());
}
this.updateById(user);
//检查角色是否越权
// checkRole(user);
//保存用户与角色关系
sysUserRoleService.saveOrUpdate(user.getUserId(), user.getRoleIdList());
}
在进到aveOrUpdate方法后,我们就可以发现因为null值,所以我在数据库当中的权限也没有了,到这里后便发现了为什么权限消失的问题
public void saveOrUpdate(Long userId, List<Long> roleIdList) {
//先删除用户与角色关系
this.removeByMap(new MapUtils().put("user_id", userId));
if(roleIdList == null || roleIdList.size() == 0){
return ;
}
修改后的代码:
1.创建user对象:在bug找到后其实就用service层的对象就ok的。创建了一个UpdateWrapper对象,用于更新数据库中的数据。 再通过Service层的getOne方法通过QueryWrapper对象查询数据库中user_id为用户登录id的记录,只返回user_id,以及冻结时间的字段并只返回一条记录
2.比较时间:先获取当前时间戳nowTImestamp(用于做比较) -->检查冻结时间是否为空
-->如果为空,计算新的冻结时间点freezeTimestamp,即当前时间加5分钟(以毫秒为单位),再将时间戳转换为Date对象 freezeUntilDate(为了返回给数据库字段) ,构建UpdateWrapper,调用eq()方法根据用户ID,set()方法更新其在数据库中的'freeze_until'字段为新的冻结结束日期
-->时间不为空,先获取用户现有的冻结时间时间戳,再与当地时间进行比较
-->大于当地时间戳则抛出异常,提示无法操作
-->小于当地时间,获取当地时间+5分钟的时间戳对象,并将时间对象调用eq()方法根据用户ID,set()方法更新其在数据库中的'freeze_until'字段为新的冻结结束日期
//1.
UpdateWrapper<SysUserEntity> updateWrapper = new UpdateWrapper<>();
//2.
SysUserEntity user = sysUserService.getOne(new QueryWrapper<SysUserEntity>().eq("user_id", userInfoVO.getUserId())
.select("user_id", "freeze_until")
.last("limit 1"));
//3.
long nowTimestamp = new Date().getTime();
if (user.getFreezeUntil() == null) {
long freezeTimestamp = nowTimestamp + 5 * 60 * 1000;
Date freezeUntilDate = new Date(freezeTimestamp);
//user.setFreezeUntil(freezeUntilDate);
updateWrapper.eq("user_id", userInfoVO.getUserId())
.set("freeze_until", freezeUntilDate)
;
} else {
long freezeUntilTimestamp = user.getFreezeUntil().getTime();
if (nowTimestamp < freezeUntilTimestamp) {
throw new IllegalStateException("当前时间小于冻结结束时间,无法操作");
} else {
// 当前时间已经超过冻结期,则重置冻结时间为当前时间+5分钟
long newFreezeTimestamp = nowTimestamp + 5 * 60 * 1000;
Date newFreezeUntilDate = new Date(newFreezeTimestamp);
// user.setFreezeUntil(newFreezeUntilDate);
updateWrapper.eq("user_id", userInfoVO.getUserId())
.set("freeze_until", newFreezeUntilDate)
;
}
}
2.2 保存对象
1.调用service层的update函数,将用户信息和更新条件封装到updateWrapper中,然后更新用户信息到数据库中。 2.将userInfoVO.getUserId()赋值给holiday对象的userId属性。3.调用iHolidayService.save(holiday)函数,将holiday对象保存到数据库中。4.返回R.ok()表示操作成功。
sysUserService.update(user, updateWrapper);
holiday.setUserId(userInfoVO.getUserId());
iHolidayService.save(holiday);
return R.ok();
完整代码
controller层
public R save(@RequestBody Holiday holiday) {
getUser();
getUserId();
//可以拿主表登录人员的数据
UserInfoVO userInfoVO = sysUserService.getLoginUserInfo();
//1.
UpdateWrapper<SysUserEntity> updateWrapper = new UpdateWrapper<>();
//2.
SysUserEntity user = sysUserService.getOne(new QueryWrapper<SysUserEntity>().eq("user_id", userInfoVO.getUserId())
.select("user_id", "freeze_until")
.last("limit 1"));
//3.
long nowTimestamp = new Date().getTime();
if (user.getFreezeUntil() == null) {
long freezeTimestamp = nowTimestamp + 5 * 60 * 1000;
Date freezeUntilDate = new Date(freezeTimestamp);
//user.setFreezeUntil(freezeUntilDate);
updateWrapper.eq("user_id", userInfoVO.getUserId())
.set("freeze_until", freezeUntilDate)
;
} else {
// 获取用户现有的冻结结束时间戳
long freezeUntilTimestamp = user.getFreezeUntil().getTime();
if (nowTimestamp < freezeUntilTimestamp) {
throw new IllegalStateException("当前时间小于冻结结束时间,无法操作");
} else {
// 当前时间已经超过冻结期,则重置冻结时间为当前时间+5分钟
long newFreezeTimestamp = nowTimestamp + 5 * 60 * 1000;
Date newFreezeUntilDate = new Date(newFreezeTimestamp);
// user.setFreezeUntil(newFreezeUntilDate);
updateWrapper.eq("user_id", userInfoVO.getUserId())
.set("freeze_until", newFreezeUntilDate)
;
}
}
//holiday.setUserId(getUserId());
//更新用户冻结时间
//sysUserService.update(user);
sysUserService.update(user, updateWrapper);
holiday.setUserId(userInfoVO.getUserId());
iHolidayService.save(holiday);
return R.ok();
}