文章目录
- 前置准备
- 介绍
- 导入依赖
- 框架结构
- 自定义注解
- 介绍
- 鉴定角色
- 鉴定权限
- 自定义异常
- AuthenticationException
- NoSuchRolesException
- NoSuchPermissionException
- 编写Verify
- Verfiy接口
- VerifyConfigurer配置类
- 使用AOP鉴权
- 失败案例
- 成功案例
- 编写前置通知
- 动态配置切点
- 打包及使用
- 打包
- 使用
- 导入AOP依赖
- 导入Jar包
- 编写配置类
- 全局异常处理
- 使用鉴权注解
- 其他问题
- 源码
前置准备
介绍
- 本项目是基于
Springboot AOP
开发的功能简单的鉴权框架,本篇文章会介绍开发流程 - 建议配合
JWT
和ThreadLocal
一起使用效果更佳 - 本框架在正式使用时需要先编写配置类,然后在经过JWT过滤的接口方法上方添加鉴定角色或鉴定权限的注解。
- 使用AOP鉴定角色和权限时,两者只要有一个不符合要求,则拒绝执行接口。同时抛出异常,在全局异常处理当这捕获处理该异常
导入依赖
因为该框架基于Springboot AOP开发,所以需要导入AOP依赖,同时在框架开发完成之后需要打包,这里也给出了打包插件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>NONE</layout>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
完整配置
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<groupId>org.zq</groupId>
<artifactId>eVerify</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>NONE</layout>
<classifier>exec</classifier>
</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
AuthenticationException
异常时该框架中的最顶级的自定义异常,该框架中其他自定义异常均继承于它
package com.zq.exception;
public class AuthenticationException extends RuntimeException{
public AuthenticationException(String message) {
super(message);
}
}
NoSuchRolesException
当用户没有指定角色时抛出该异常
package com.zq.exception;
public class NoSuchRolesException extends AuthenticationException {
public NoSuchRolesException(String message) {
super(message);
}
}
NoSuchPermissionException
当用户没有该权限时抛出该异常
package com.zq.exception;
public class NoSuchPermissionException extends AuthenticationException {
public NoSuchPermissionException(String message) {
super(message);
}
}
编写Verify
Verfiy接口
编写一个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配置类
编写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 List<String> getRoles()
查询用户角色的方法,后续需要程序员自己重写该方法
public List<String> getPermissions()
查询用户权限的方法,后续需要程序员自己重写该方法
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, List<String> target)
查询src
和target
中是否存在交集,即有没有相同角色,有则返回true
,没有返回fasle
public boolean checkAND(String[] src, List<String> target)
查询src∈target
是否为真,即src
中的所有角色在target
中都能找到,都能则返回true
,否则返回false
使用AOP鉴权
失败案例
之前我是使用最常见的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包所在的路径:
使用
导入AOP依赖
该框架基于Springboot AOP开发,所以需要导入该依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
导入Jar包
将上面打包生成的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
新手上路,有问题请指正,谢谢