SpringBoot基于RBAC自制简易的权限验证(AOP自定义注解)
之前的博客介绍了RBAC的原理,这个博客介绍一下springboot如何基于RBAC自制简易的权限验证
流程介绍
这里做了一些简单的修改,比如redis存入的key是token,value是对应的user对象
这里没有对应的权限资源表,所以使用aop切面注解进行权限比较的时候,使用固定的list比较
正常应该通过账号,查询角色,通过角色查询对应的资源信息
坐标依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<!-- aop相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
</dependencies>
登录认证
登录函数
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.校验验证码
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
// Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
// select * from tb_user where phone = ?
//一致,根据手机号查询用户(query获取表名通过User实体的@tableName注解,ServiceImpl<UserMapper, User>)
//one返回一个对象,list,返回集合
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user == null){
//不存在,则创建
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
// BeanUtil.copyProperties(user, UserDTO.class)(hutol工具类,把user的内容拷贝到UserDto)
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
//7.保存用户信息到redis中
// 7.1.随机生成token,作为登录令牌(使用hutol提供的UUid,toString(true)标识生成的没有-符号,toString()有)
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
//赋值user给userDto(去掉敏感信息)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//转为HashMap存储(因为stringRedisTemplate要求key和value存储的都是string类型,需要转换)
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)//忽略一些空值
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//setFieldValueEditor传入字段名和字段值,只要字符串形式的字段值
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期(hash不能直接设置)
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
配置拦截器
这里我用到了两个拦截器,因为我的token存在redis里,不能定时刷新,所以一个拦截器用来刷新redis(拦截全部请求),一个拦截用来拦截全部请求,验证用户
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截,设置状态码
response.setStatus(401);
// 拦截
return false;
}
// 有用户,则放行
return true;
}
}
RefreshTokenInterceptor
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
//这个类没有加上@compentent类似的注解,该类对象的属性注入不有spring来完成,只能通过构造方法的方式,来初始化成员变量
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//.if生成if表达式 .var生成变量
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
//这里不拦截,第二个拦截器进行拦截
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
//entries取出该hash的key的全部字段值,返回值是map集合,get只能取出key的单个字段值,返回值是Object
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在,opsForHash如果为null会返回一个空的map,所以这里不判断Null
//这个拦截器不拦截
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
MvcConfig(拦截器配置)
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(//排除下面不需要拦截
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
//order()配置这个拦截器执行顺序,值越大,优先级越低
// token刷新的拦截器
//,addPathPatterns//添加拦截/**拦截全部,默认不写也是拦截全部请求
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
以上为登录判断,存入token
下面,来介绍如何通过aop切面的方式来解决是否含有这个权限
资源认证
其中aop依赖
<!-- aop相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义注解
自定义了几个常量
public class PermissionConsts {
/**
* 查看权限
*/
public static final String R = "R_PERMISSION";
/**
* 添加权限
*/
public static final String C = "C_PERMISSION";
/**
* 修改权限
*/
public static final String U = "U_PERMISSION";
/**
* 删除权限
*/
public static final String D = "D_PERMISSION";
}
MyPermission
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPermission {
/**
* 默认只有查看权限
* @return
*/
String value() default PermissionConsts.R;
}
- @Target,说明了Annotation所修饰的对象范围
- @Retention,定义了该Annotation生命周期(编译/运行)
- @Documented,是一个标记注解,没有成员
- @Inherited,阐述了某个被标注的类型是被继承的。
AOP切面
@Aspect
表示这是一个切面类
@Around("@annotation(com.hmdp.annotation.MyPermission)")
里面表示在注解处环绕通知。
@Slf4j
是Lombok的关于slfj的简略写法。
@Aspect
@Component
@Slf4j
public class PermissionAspect {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ResourceVerification resourceVerification;
/**
* 目标方法
*/
@Pointcut("@annotation(com.hmdp.annotation.MyPermission)")
private void permission() {
}
/**
* 目标方法调用之前执行
*/
@Before("permission()")
public void doBefore() {
System.out.println("================== step 2: before ==================");
}
/**
* 目标方法调用之后执行
*/
@After("permission()")
public void doAfter() {
System.out.println("================== step 4: after ==================");
}
/**
* 环绕
* 会将目标方法封装起来
* 具体验证业务数据
*/
@Around("permission()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("================== step 1: around ==================");
long startTime = System.currentTimeMillis();
/*
* 解析token :
* 1、token是否存在
* 2、token格式是否正确
* 3、token是否已过期(解析信息或者redis中是否存在)
* */
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isEmpty(token)) {
throw new RuntimeException("非法请求,无效token");
}
// 校验token的业务逻辑\
// 基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
//entries取出该hash的key的全部字段值,返回值是map集合,get只能取出key的单个字段值,返回值是Object
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
if (userMap.isEmpty()) {
// throw new RuntimeException("非法请求,无效用户");
System.out.println("非法请求,无效用户");
}
// 解析token之后,获取当前用户的账号信息,查看它对应的角色和权限信息
// 将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
//查询,用户的权限信息
Long userId = userDTO.getId();
// List<String> permissionCodes = userService.getResource(userId);
// 这块没有对应的数据表,用写死的集合表示。
/*
* 获取注解的值,并进行权限验证
* */
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
MyPermission myPermission = method.getAnnotation(MyPermission.class);
String value = myPermission.value();
// 将注解的值和token解析后的值进行对比,查看是否有该权限,如果权限通过,允许访问方法;否则不允许,并抛出异常
//这里写死比较,实际查询数据库
if(!resourceVerification.compareResource(value)){
// throw new RuntimeException("对不起,您没有权限访问!");
System.out.println("对不起,您没有权限访问!");
}
// 执行具体方法
Object result = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
/*
* 记录相关执行结果
* 可以存入MongoDB 后期做数据分析
* */
// // 打印请求 url
// System.out.println("URL : " + request.getRequestURL().toString());
// // 打印 Http method
// System.out.println("HTTP Method : " + request.getMethod());
// // 打印调用 controller 的全路径以及执行方法
// System.out.println("controller : " + proceedingJoinPoint.getSignature().getDeclaringTypeName());
// // 调用方法
// System.out.println("Method : " + proceedingJoinPoint.getSignature().getName());
// // 执行耗时
// System.out.println("cost-time : " + (endTime - startTime) + " ms");
return result;
}
}
这里没有去查询数据库查询用户的资源,二是写了一个死的集合来判断
ResourceVerification
@Component
public class ResourceVerification {
//权限比较
private List<String> resources = new ArrayList<>();
public Boolean compareResource(String temp){
initResources();
if (resources.contains(temp)){
return true;
}
return false;
}
public void initResources(){
resources.add("/user/me");
resources.add("/user/test");
resources.add("/test/me");
resources.add("/test/test");
resources.add("/test/me");
}
}
测试函数
@GetMapping("/me")
@MyPermission("/user/me122")
// @MyPermission自定义切面注解
public Result me(){
// TODO 获取当前登录的用户并返回(从ThreadLocal中)
UserDTO user = UserHolder.getUser();
// return Result.fail("功能未完成");
return Result.ok(user);
}
本文含有隐藏内容,请 开通VIP 后查看