前言:
(1)假设现在有一个项目,用户只需登录就可以访问所有接口。基于这种场景,我们一般的权限鉴权逻辑是:用户登录后生成一个随机token保存到redis并返回给用户端,用户随后的每次请求都会携带这个token。服务器配置拦截器拦截用户请求,查看请求是否携带token并且查询token是否有效,若请求没有携带token或者token过期,直接拒绝请求,若token有效则刷新token的过期时间,放行请求。
(2)随着项目业务升级,用户可以充值办理会员,项目中的一些接口现在只有会员才能访问,而拦截器只能拦截未登录用户的请求,此时就需要我们在需要会员权限的接口加一些权限判断逻辑(需要改动代码)。有没有一种方法在不改动代码的情况下,对请求进行更进一步的权限校验呢?这就需要我们今天的”主角“AOP了。
思路:
不再通过拦截的方式实现接口鉴权,而是在需要鉴权的接口上添加自定义注解,通过Spring AOP对标注了自定义注解的接口进行逻辑增强,在执行真正的业务处理逻辑前校验token是否存在且有效、用户是否具有相应权限,若校验通过再执行真正的业务处理逻辑。
如何自定义注解?
(1)注解关键字:@interface。
(2)@Target:注解的作用范围。该注解有一个value属性,其类型是ElementType数组,你可以声明这个注解作用在多种类型上,如下所示。
//FIELD:作用在字段上,METHOD:作用在方法上,TYPE:作用在类上
@Target(value = {ElementType.FIELD,ElementType.METHOD,ElementType.ANNOTATION_TYPE})
(3)@Retention:注解的生命周期。
(4)value:String类型,表示该接口需要的权限类型,有普通用户、会员这两种权限类型。
//注解需作用在方法上
@Target(ElementType.METHOD)
//注解会保留到程序运行期
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
//这是访问接口时,用户需要拥有的权限
//用户有两种角色:普通用户、会员
String value() default "";
}
权限校验流程
(1)在pom文件中引入aop依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(2)定义切面类:
//声明一个类为切面类
@Aspect
//将该类注入到IOC容器中,交给Spring管理
@Component
public class AuthorityAspectConfig {
@Resource
private StringRedisTemplate template;//spring封装的redis客户端
@Resource
ObjectMapper mapper;//json转换工具
/**
* 声明一个切入点
* 切入点为:所有标注了Authority注解的方法
*/
@Pointcut("@annotation(com.hammajang.springbootdemo.config.Aspect.Authority)")
public void authority(){
}
/**
* 具体验证业务数据
*/
@Around("authority()")
public void doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long start = System.currentTimeMillis();
//1.判断请求是否携带token
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("用户未登录,请先登录");
}
//2.校验token是否过期
//2.1通过token从redis中取出用户信息
String userStr = template.opsForValue().get(token);
//2.2用户为null,说明是无效token
if (userStr == null){
throw new RuntimeException("非法请求,token无效");
}
User user = mapper.readValue(userStr, User.class);
//2.3不为null,则token未过期,刷新token...
template.opsForValue().set(token,userStr,30, TimeUnit.MINUTES);
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
Authority authority = method.getAnnotation(Authority.class);
//获取注解的值
String value = authority.value();
//如果需要会员权限,判断用户是否是会员
if(value.equals("1") && user.getRole() != 1)
throw new RuntimeException("用户没有足够权限!");
// 执行真正的业务处理逻辑
Object result = proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
//打印接口耗时
System.out.println(proceedingJoinPoint.getSignature().getName() + "接口执行总耗时:" + (end - start) +"ms");
}
}
(3)标注需要权限权限的接口:
@RestController
@RequestMapping("/coupon")
public class CouponController {
//普通用户:0
//会员:1
@Authority("1")
@GetMapping("/test")
public String checkCoupon(){
System.out.println("需要用户是会员的优惠券接口...");
return "success";
}
}
测试注解是否生效
使用postman工具测试
(0)提前通过测试类在redis中保存一个用户的信息。
@SpringBootTest
class SpringbootDemoApplicationTests {
@Resource
StringRedisTemplate template;
@Test
void test() throws JsonProcessingException {
User user = new User();
user.setId(3);
user.setRole(0);
ObjectMapper mapper = new ObjectMapper();
//将对象转成JSON格式
String jsonStr = mapper.writeValueAsString(user);
//去除UUID自带的横杠
char[] chars = UUID.randomUUID().toString().toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < chars.length; i++)
if(chars[i] != '-')
sb.append(chars[i]);
String token = sb.toString();
//将随机生成的token写入redis,并设置过期时间为30min
template.opsForValue().set(token,jsonStr,30, TimeUnit.MINUTES);
}
}
token及用户信息成功保存在redis中:
(1)请求不携带token的情况:
控制台打印:
(2)请求携带token,但是token无效的情况:
控制台打印:
(3)请求携带有效token,但用户权限不够的情况:
控制台打印:
(4)修改用户权限为1,再次访问该接口:
控制台打印:
经过测试自定义注解有效,至此就完成了基于自定义注解+AOP的形式实现接口鉴权的目的,如果有什么错误的地方,请在评论区指正!