文章目录

  • 前置准备
  • 介绍
  • 导入依赖
  • 框架结构
  • 自定义注解
  • 介绍
  • 鉴定角色
  • 鉴定权限
  • 自定义异常
  • AuthenticationException
  • NoSuchRolesException
  • NoSuchPermissionException
  • 编写Verify
  • Verfiy接口
  • VerifyConfigurer配置类
  • 使用AOP鉴权
  • 失败案例
  • 成功案例
  • 编写前置通知
  • 动态配置切点
  • 打包及使用
  • 打包
  • 使用
  • 导入AOP依赖
  • 导入Jar包
  • 编写配置类
  • 全局异常处理
  • 使用鉴权注解
  • 其他问题
  • 源码


前置准备

介绍

  • 本项目是基于Springboot AOP开发的功能简单的鉴权框架,本篇文章会介绍开发流程
  • 建议配合JWTThreadLocal一起使用效果更佳
  • 本框架在正式使用时需要先编写配置类,然后在经过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>

框架结构

以下是框架编写完成之后的结构

axios 鉴权 跳转 登录页 aop实现鉴权_spring

自定义注解

介绍

在学习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) 查询srctarget中是否存在交集,即有没有相同角色,有则返回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)中字符数组的地址,确实修改成功了,但可能是因为底层使用了动态代理,即使使用反射也无法改变切入点,所以这种写法失败了,下面介绍更加灵活的一种写法

成功案例

编写前置通知

继承MethodBeforeAdviceAdvisorAdapter接口,实现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打包工具打包:

axios 鉴权 跳转 登录页 aop实现鉴权_List_02


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

axios 鉴权 跳转 登录页 aop实现鉴权_List_03

使用

导入AOP依赖

该框架基于Springboot AOP开发,所以需要导入该依赖:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

导入Jar包

将上面打包生成的Jar包,或者直接点击链接下载eVerify-1.0.jar 导入到springboot项目中

axios 鉴权 跳转 登录页 aop实现鉴权_axios 鉴权 跳转 登录页_04

可以直接复制粘贴到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中计算的usernameuserId查询数据库,校验是否有角色或者权限,以此判断该用户是否有权限访问该接口
4.若使用JWT框架,请不要在未经过JWT过滤的接口上使用鉴权注解
5.一般来说是从token中拿user信息,在getRolesgetPermissions方法里面用user信息查数据库

源码

框架地址 :eVerify
新手上路,有问题请指正,谢谢