Springboot AOP开发的功能简单的鉴权框架,本篇文章会介绍开发流程JWT和ThreadLocal一起使用效果更佳因为该框架基于Springboot AOP开发,所以需要导入AOP依赖,同时在框架开发完成之后需要打包,这里也给出了打包插件
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<layout>NONElayout>
<classifier>execclassifier>
configuration>
plugin>
plugins>
build>
完整配置
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.zqgroupId>
<artifactId>eVerifyartifactId>
<version>1.0version>
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.4version>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<layout>NONElayout>
<classifier>execclassifier>
configuration>
plugin>
plugins>
build>
project>
以下是框架编写完成之后的结构

在学习shiro框架之后,发现注解鉴权写法非常方便,这里直接参考shiro框架编写了鉴定角色和鉴定权限的注解
编写CheckRoles注解,用于鉴定用户有没有该角色,当用户没有相应角色时抛出异常,msg字段属性作为异常的e.message信息,在使用注解时可以手动设置msg的值,同时抛出异常时也使用手动设置的值,type字段相当于选择条件是||还是&&,即有其中一个角色或者全部角色时为真
注意:未实现通配符匹配
import com.zq.annotation.type.CheckRolesType;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckRoles {
String[] value();
String msg() default "Check role failed,maybe you don't have role(s) to access the current interface";
CheckRolesType type() default CheckRolesType.OR;
}
package com.zq.annotation.type;
public enum CheckRolesType {
AND, OR
}
编写CheckPermission注解,用于鉴定用户有没有该权限,当用户没有相应权限时抛出异常,msg字段属性作为异常的e.message信息,在使用注解时可以手动设置msg的值,同时抛出异常时也使用手动设置的值,type字段相当于选择条件是||还是&&,即有其中一个权限或者全部权限时为真
注意:未实现通配符匹配
import com.zq.annotation.type.CheckPermissionType;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckPermission {
String[] value();
String msg() default "Check permission failed,maybe you don't have permission(s) to access the current interface";
CheckPermissionType type() default CheckPermissionType.OR;
}
package com.zq.annotation.type;
public enum CheckPermissionType{
AND,OR
}
AuthenticationException异常时该框架中的最顶级的自定义异常,该框架中其他自定义异常均继承于它
package com.zq.exception;
public class AuthenticationException extends RuntimeException{
public AuthenticationException(String message) {
super(message);
}
}
当用户没有指定角色时抛出该异常
package com.zq.exception;
public class NoSuchRolesException extends AuthenticationException {
public NoSuchRolesException(String message) {
super(message);
}
}
当用户没有该权限时抛出该异常
package com.zq.exception;
public class NoSuchPermissionException extends AuthenticationException {
public NoSuchPermissionException(String message) {
super(message);
}
}
编写一个Verify接口,分别声明校验角色和校验权限的接口方法
package com.zq.verify;
import com.zq.annotation.type.CheckPermissionType;
import com.zq.annotation.type.CheckRolesType;
public interface Verify {
boolean verifyRoles(String[] roles, CheckRolesType type);
boolean verifyPermissions(String[] permissions, CheckPermissionType type);
}
boolean verifyRoles(String[] roles, CheckRolesType type);
校验用户是否有该角色,roles为来自CheckRoles注解里面的String[] value()值
type是使用CheckRoles时设置的逻辑关系,没有设置则默认为OR
- 若
type==OR,则只要用户有String[] roles里面任意一个角色,就返回true- 若
type==AND,则需要用户有String[] roles里面的所有角色,才返回true
boolean verifyPermissions(String[] permissions, CheckPermissionType type);
校验用户是否有该权限,permissions为来自CheckPermissions注解里面的String[] value()值
type是使用CheckPermissions时设置的逻辑关系,没有设置则默认为OR
- 若
type==OR,则只要用户有String[] roles里面任意一个角色,就返回true- 若
type==AND,则需要用户有String[] roles里面的所有角色,才返回true
编写VerifyConfigurer配置类,继承于Verify接口,并实现接口方法
package com.zq.verify;
import com.zq.annotation.type.CheckPermissionType;
import com.zq.annotation.type.CheckRolesType;
import java.util.List;
public class VerifyConfigurer implements Verify {
public List<String> getRoles(){
return null;
}
public List<String> getPermissions(){
return null;
}
/**
* 校验用户是否拥有角色
* @param roles
* @param type
* @return
*/
@Override
public boolean verifyRoles(String[] roles, CheckRolesType type) {
if(roles==null || roles.length==0) return true;
final List<String> rolesList = getRoles();
if(rolesList==null || rolesList.size()==0) return false;
if(type==CheckRolesType.OR)
return checkOR(roles,rolesList);
return checkAND(roles,rolesList);
}
/**
* 校验用户是否拥有权限
* @param permissions
* @param type
* @return
*/
@Override
public boolean verifyPermissions(String[] permissions, CheckPermissionType type) {
if(permissions ==null || permissions.length==0) return true;
final List<String> permissionsList = getPermissions();
if(permissionsList ==null || permissionsList.size()==0) return false;
if(type==CheckPermissionType.OR)
return checkOR(permissions,permissionsList );
return checkAND(permissions,permissionsList);
}
public boolean checkOR(String[] src, List<String> target) {
for (String role : src) {
if (target.contains(role)) {
return true;
}
}
return false;
}
public boolean checkAND(String[] src, List<String> target){
for (String role : src) {
if (!target.contains(role)) {
return false;
}
}
return true;
}
}
public ListgetRoles()
查询用户角色的方法,后续需要程序员自己重写该方法
public ListgetPermissions()
查询用户权限的方法,后续需要程序员自己重写该方法
public boolean verifyRoles(String[] roles, CheckRolesType type)
使用getRoles()方法查询用户角色,并与传进来的roles参数进行逻辑匹配,参数type为匹配逻辑,匹配失败返回false,成功返回true
public boolean verifyPermissions(String[] permissions, CheckPermissionType type)
使用getPermissions()方法查询用户权限,并与传进来的permissions参数进行逻辑匹配,参数type为匹配逻辑,匹配失败返回false,成功返回true
public boolean checkOR(String[] src, Listtarget)
查询src和target中是否存在交集,即有没有相同角色,有则返回true,没有返回fasle
public boolean checkAND(String[] src, Listtarget)
查询src∈target是否为真,即src中的所有角色在target中都能找到,都能则返回true,否则返回false
之前我是使用最常见的springboot aop写法,使用以下写法
@Pointcut(value = "execution(* com.zq.drawingBed.controller..*.*(..))")
public void pointCut(){}
但是上面的写法不太灵活,无法将value属性抽取出来放入配置文件中再注入进去,所以我尝试在项目启动的时候使用反射获取
public void pointCut(){}方法上面的@Pointcut注解的value属性,然后使用反射修改了value对象(String)中字符数组的地址,确实修改成功了,但可能是因为底层使用了动态代理,即使使用反射也无法改变切入点,所以这种写法失败了,下面介绍更加灵活的一种写法
继承MethodBeforeAdvice和AdvisorAdapter接口,实现controller接口方法调用前的拦截
package com.zq.aop;
import com.zq.annotation.CheckPermission;
import com.zq.annotation.CheckRoles;
import com.zq.exception.NoSuchPermissionException;
import com.zq.exception.NoSuchRolesException;
import com.zq.verify.Verify;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.adapter.AdvisorAdapter;
import java.lang.reflect.Method;
/**
* 自定义前置AOP
*/
public class VerifyBeforeAdvice implements MethodBeforeAdvice, AdvisorAdapter {
private Verify verify;
public void setVerify(Verify verify) {
this.verify = verify;
}
@Override
public void before(Method method, Object[] args, Object target) {
final CheckRoles rolesAnnotation = method.getAnnotation(CheckRoles.class);
final CheckPermission permissionAnnotation = method.getAnnotation(CheckPermission.class);
if(rolesAnnotation!=null&&
!verify.verifyRoles(rolesAnnotation.value(), rolesAnnotation.type()))
throw new NoSuchRolesException(rolesAnnotation.msg());
if(permissionAnnotation!=null&&
!verify.verifyPermissions(permissionAnnotation.value(), permissionAnnotation.type()))
throw new NoSuchPermissionException(permissionAnnotation.msg());
}
@Override
public boolean supportsAdvice(Advice advice) {
return true;
}
@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
return null;
}
}
private Verify verify;
程序员使用该框架的时候,需要重写getRoles()和getPermissions方法,将重写后的class对象注入到上面的字段里面去
public void before(Method method, Object[] args, Object target)
controller接口方法调用前的拦截时调用的方法,在该方法中使用verify对象进行角色和权限校验
编写一个建造者类,用于动态设置切点,并生成AOP对象
package com.zq.aop;
import com.zq.verify.Verify;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
/**
* 自定义AOP
*/
public class VerifyPointCutAdvisorBulider {
public VerifyPointCutAdvisorBulider() {
}
public VerifyPointCutAdvisorBulider(Verify verify, String controllerPath) {
this.verify = verify;
this.controllerPath = controllerPath;
}
private Verify verify;
private String controllerPath;
public Verify getVerify() {
return verify;
}
public String getControllerPath() {
return controllerPath;
}
public void setVerify(Verify verify) {
this.verify = verify;
}
public void setControllerPath(String controllerPath) {
this.controllerPath = controllerPath;
}
private Pointcut createPointCut(String controllerPath) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public * "+controllerPath+"..*(..))");
return pointcut;
}
private VerifyBeforeAdvice createAdvice () {
VerifyBeforeAdvice beforeAdvice = new VerifyBeforeAdvice();
beforeAdvice.setVerify(verify);
return beforeAdvice;
}
public DefaultPointcutAdvisor bulid () {
DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
Pointcut pointCut = createPointCut(controllerPath);
defaultPointcutAdvisor.setPointcut(pointCut);
VerifyBeforeAdvice beforeAdvice = createAdvice();
defaultPointcutAdvisor.setAdvice(beforeAdvice);
return defaultPointcutAdvisor;
}
}
private Verify verify;
程序员使用该框架的时候,需要重写getRoles()和getPermissions方法,将重写后的class对象注入到上面的字段里面去
private String controllerPath;
动态配置的注入点,这里是将项目中的controller包所在的路径当成注入点使用
private Pointcut createPointCut(String controllerPath)
使用controllerPath,创建切点对象
private VerifyBeforeAdvice createAdvice ()
创建AOP前置通知操作对象,并将重写了getRoles()和getPermissions()方法的verify对象注入进去
public DefaultPointcutAdvisor bulid ()
使用建造者模式,构建AOP对象
使用IDEA自带的maven打包工具打包:

打包成功之后控制台显示jar包所在的路径:

该框架基于Springboot AOP开发,所以需要导入该依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
将上面打包生成的Jar包,或者直接点击链接下载eVerify-1.0.jar
导入到springboot项目中

可以直接复制粘贴到
lib目录下,右键选中该jar包,点击添加为库
在config目录下创建VerfiyConfig配置类,继承于VerifyConfigurer:
getRoles()和getPermissions()方法,这里注入了UserService,用来查询数据库,以实现上面两个方法controller包所在的项目路径(作为鉴权AOP的切入点)VerifyPointCutAdvisorBulider配置鉴权AOP对象,并注入到IOC容器中package com.zq.config;
import com.zq.aop.VerifyPointCutAdvisorBulider;
import com.zq.service.impl.UserService;
import com.zq.verify.VerifyConfigurer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Slf4j
@Configuration
public class VerifyConfig extends VerifyConfigurer {
@Autowired
private UserService userService;
private String controllerPath="com.zq.controller";
public List<String> getRoles() {
return userService.getRoles("admin");
}
@Override
public List<String> getPermissions() {
return userService.getPermissions("admin");
}
@Bean(value = "AuthenticationAop")
public DefaultPointcutAdvisor createDefaultPointcutAdvisor(){
log.info("鉴权配置启动");
VerifyPointCutAdvisorBulider bulider=new VerifyPointCutAdvisorBulider();
bulider.setVerify(this);
bulider.setControllerPath(controllerPath);
return bulider.bulid();
}
}
该框架中编写了三个自定义异常,其中两个军继承于AuthenticationException异常,所以可以直接捕获该异常进行处理,e.getMessage()的内容为注解里面设置的msg的参数内容,当校验失败的时候使用msg作为异常的message内容
package com.zq.exception;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {AuthenticationException.class})
public String handleAuthenticationException(AuthenticationException e) {
return e.getMessage();
}
}
上面的代码要根据自己的项目修改返回值类型,这里为了演示写成了String
package com.zq.controller;
import com.zq.annotation.CheckPermission;
import com.zq.annotation.CheckRoles;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(value = "test", method = RequestMethod.GET)
public class TestController {
@CheckRoles("admin")
@CheckPermission("user:add")
@GetMapping(value = "t01")
public String t01() {
return "01";
}
@CheckRoles("admin")
@CheckPermission("user:edit")
@GetMapping(value = "t02")
public String t02() {
return "02";
}
@CheckRoles("admin")
@GetMapping(value = "t03")
public String t03() {
return "03";
}
@CheckRoles("user")
@GetMapping(value = "t04")
public String t04() {
return "04";
}
@CheckPermission("user:add")
@GetMapping(value = "t05")
public String t05() {
return "05";
}
}
测试接口



该框架建议配合JWT一起使用,校验流程可以如下:
1.用户成功登录之后返回token
2.用户下次请求时携带token,在JWT过滤器中校验token是否有效
从
token中拿的user信息建议放到Threadlocal里面,下次其他方法需要的时候二次利用
3.eVerify框架在AOP中获取被调用接口方法标注的角色和权限注解,根据从token中计算的username或userId查询数据库,校验是否有角色或者权限,以此判断该用户是否有权限访问该接口
4.若使用JWT框架,请不要在未经过JWT过滤的接口上使用鉴权注解
5.一般来说是从token中拿user信息,在getRoles或getPermissions方法里面用user信息查数据库
框架地址 :eVerify
新手上路,有问题请指正,谢谢