使用【注解】【AOP】【过滤器】实现权限控制

发布时间 2023-10-31 15:15:05作者: 乔京飞

前面介绍了注解加拦截器的权限控制方式,在拦截器中解析注解配置进行权限控制。拦截器的方案:优点是比较简洁,缺点是只能在 controller 及其下面的方法配置注解控制权限。已经可以满足绝大多数项目的需求。

本篇博客介绍第二种方案,在 AOP 切面中解析注解配置进行权限控制。AOP 方案,优点是可以通过配置切入点,在任何类以及下面的方法上配置注解控制权限,缺点是实现方案稍微繁琐一点点。

两种方案各有优缺点,主要根据自己项目的实际情况而定。当然也可以使用拦截器和 AOP 相结合的方案,这里就不展示了。


一、搭建工程

搭建一个 SpringBoot 工程,其结构如下所示:

image

CheckPower 是自定义的注解,用来配置访问资源所需要的权限信息

CheckPowerAspect 是自定义的 AOP 类,用于解析类和方法上配置的注解权限

CurrentUser 是自定义的 ThreadLocal 对象,主要用于在一次请求中共享存储信息,在本博客的 Demo 中主要用途为:每次请求都会经过 LoginCheckFilter 过滤器进行登录验证处理,如果用户已经登录,则将用户信息存储到 CurrentUser 中,在 AOP 处理类中可以从 CurrentUser 中获取到用户信息

Result 是自定义的返回结果类,统一使用该类的实例对象生成 json 返回给前端

WebMvcConfig 是对网站相关的配置,包括:静态资源放行、knife4j 接口文档配置

controller 下面的类都是对外提供的接口,我们会在这里的类和方法上使用注解配置访问权限

LoginCheckFilter 是自定义的过滤器,用于拦截用户的请求,验证用户是否登录

先看一下 pom 文件引入的依赖包:

<?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>com.jobs</groupId>
    <artifactId>springboot_annotion2</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 AOP 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--引入 knife4j 依赖,使用接口文档的调试功能进行测试验证-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--里面有很多非常实用的工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

这里主要引入了 2 个关键的依赖包:spring-boot-starter-aop 和 knife4j-spring-boot-starter。本 Demo 仍然不使用相关的 web 页面进行测试,相比上一篇博客的 Demo 而言,application.yml 和 Result 类没有任何变化,内容如下:

server:
  port: 8888
  servlet:
    session:
      # 设置 session 有效期为 10 分钟
      timeout: 10m

knife4j:
  # 是否启用增强版功能
  enable: true
  # 如果是生产环境,将此设置为 true,然后就能够禁用了 knife4j 的页面
  production: false

# 自定义的用户配置
user:
  username: jobs
  password: 123
  powerlist: delorder,adduser,admin
package com.jobs.common;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

@Data
public class Result<T> implements Serializable {

    //状态码
    private Integer status;

    //消息
    private String msg;

    //返回的数据
    private T data;

    public static <T> Result<T> success(T object) {
        Result<T> r = new Result<T>();
        r.status = 0;
        r.msg = "success";
        r.data = object;
        return r;
    }

    public static <T> Result<T> fail(Integer status, String msg) {
        Result r = new Result();
        r.status = status;
        r.msg = msg;
        return r;
    }

    public static <T> Result<T> error(String msg) {
        Result r = new Result();
        r.status = 500;
        r.msg = msg;
        return r;
    }
}

下面列出自定义的 ThreadLocal 类 CurrentUser,用于在一次请求中所经过的所有方法中共享信息

package com.jobs.common;

//基于 ThreadLocal 封装的工具类,可以在一次请求过程中,在各个方法中共享访问
public class CurrentUser {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void setCurrentUser(String user) {
        threadLocal.set(user);
    }

    public static String getCurrentUser() {
        return threadLocal.get();
    }
}

二、过滤器、注解、AOP

对于 SpringBoot 来说,使用过滤器主要有 2 个步骤:

首先在启动类上,添加 @ServletComponentScan 注解,因为过滤器是基于 Servlet 实现的

package com.jobs;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@Slf4j
@ServletComponentScan
@SpringBootApplication
public class MainApp {
    public static void main(String[] args) {
        SpringApplication.run(MainApp.class, args);
        log.info("程序已经启动...");
    }
}

然后创建自定义过滤器 LoginCheckFilter 即可,在过滤器上使用 @WebFilter 注解配置好过滤器的名称和要监听的请求地址,一版情况下我们都会监听所有的请求地址。如果编写了多个过滤器的话,对于使用注解配置的过滤器,无法配置多个过滤器的执行优先级,绝大多数情况下,我们只需要一个过滤器。

package com.jobs.filter;

import com.alibaba.fastjson.JSON;
import com.jobs.common.CurrentUser;
import com.jobs.common.Result;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//执行顺序:过滤器 > 拦截器 > AOP
//判断用户是否登录,这里使用过滤器,具有最高执行的优先级
@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //放行不需要登录就可以使用的 url 路径
        String requestURI = request.getRequestURI();
        if (checkPassUri(requestURI)) {
            filterChain.doFilter(request, response);
            return;
        }

        //如果 session 存在,则表明已经登录过了
        Object user = request.getSession().getAttribute("user");
        if (user != null) {
            CurrentUser.setCurrentUser(user.toString());
            filterChain.doFilter(request, response);
            return;
        }

        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(Result.fail(-99, "请登录后再访问")));
    }


    private static final AntPathMatcher apm = new AntPathMatcher();

    private boolean checkPassUri(String requestURI) {
        String[] uris = new String[]{
                //放行用户登录接口
                "/user/login",
                //放行用户退出接口
                "/user/logout",
                //放行下面的 knifefj 的静态资源文件路径
                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs"
        };

        for (String uri : uris) {
            boolean match = apm.match(uri, requestURI);
            if (match) {
                return true;
            }
        }

        return false;
    }
}

我们自定义的 @CheckPower 注解,相比上篇博客的 Demo 没有任何变化,内容如下:

package com.jobs.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPower {

    //需要具备的权限列表,默认情况下数据组只有一个空字符串元素,表示登录后不需要验证权限就可以访问
    String[] power() default "";

    //如果配置了多个权限,之间是 and 关系,还是 or 关系
    //如果是 and 关系,则表示登录的用户,必须同时具备所配置的多个权限,才能访问
    //如果是 or 关系,则表示登录的用户,只要具备所配置的权限列表中的任意一个权限,就可以访问
    String loggic() default "or";
}

我们自定义的 AOP 切面类 CheckPowerAspect 监听 controller 包下的所有 public 方法,因此在对外提供的 controller 类中,如果不是对外提供的接口方法,则建议使用其他修饰符(如:protected 或 private)

package com.jobs.aspect;

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jobs.annotation.CheckPower;
import com.jobs.common.CurrentUser;
import com.jobs.common.Result;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;

import javax.el.TypeConverter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

//执行顺序:过滤器 > 拦截器 > AOP
//这里使用 AOP,用户请求过来之后,先执行过滤器判断用户是否登录
//如果用户登录后,再执行 AOP 判断用户是否具有执行方法的权限
@Aspect
@Component
public class CheckPowerAspect {

    //监控 com.jobs.controller 包以及其所有子包中的 public 方法
    @Pointcut("execution(public * com.jobs.controller..*(..))")
    public void pt() {
        //空方法,目的是为了挂载切入点
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {

        //实现逻辑:
        //类上有 CheckPower 注解的话,就验证是否登录,然后验证类和方法的权限;
        //类上没有 CheckPower 注解的话,就什么都不验证,直接执行原始方法

        //获取【类】上的注解,看是否存在 CheckPower 注解
        CheckPower cpClass = pjp.getTarget().getClass().getAnnotation(CheckPower.class);
        if (cpClass != null) {

            System.out.println("访问的【类】上有 @CheckPower 注解,开始验证权限...");

            //获取用户信息
            Object user = CurrentUser.getCurrentUser();
            if (user == null) {
                //用户需要登录,才能获取到用户的权限,才能去验证权限是否满足
                return Result.fail(-99, "请登录后再访问");
            }

            //获取用户具有的权限
            JSONObject userMap = JSON.parseObject(user.toString());
            JSONArray jsonArray = userMap.getJSONArray("powerlist");
            String[] powerlist = jsonArray.toArray(new String[jsonArray.size()]);

            //【类】上配置的权限是否满足,默认为 true 表示满足
            boolean classCheckPowerFlag = true;

            //获取配置的权限列表
            System.out.println("【类】需要的权限为:" + Arrays.toString(cpClass.power()) +
                    ",权限逻辑关系:" + cpClass.loggic());
            System.out.println("用户具有的权限:" + Arrays.toString(powerlist));
            //如果【类】上有 @CheckPower 注解,并且没有配置任何权限,则表示登录后可以随便访问
            if (cpClass.power().length == 1 && cpClass.power()[0].equals("")) {
                classCheckPowerFlag = true;
            } else {
                classCheckPowerFlag =
                        getCheckPowerResult(powerlist, cpClass.power(), cpClass.loggic());
            }

            //判断方法上是否有 @CheckPower 注解
            CheckPower cpMethod =
                    ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(CheckPower.class);
            if (cpMethod != null) {
                System.out.println("访问的 Method 方法上有 @CheckPower 注解...");
                //获取配置的权限列表
                System.out.println("Method 需要的权限为:" + Arrays.toString(cpMethod.power())
                        + ",权限逻辑关系:" + cpMethod.loggic());
                System.out.println("用户具有的权限:" + Arrays.toString(powerlist));
                //如果方法上有 @CheckPower 注解,并且没有配置任何权限,则表示登录后可以随便访问
                //之所以这样做,是为了能够在 Controller 上设置了权限后,对其下面的个别方法可以放开权限。
                if (cpMethod.power().length == 1 && cpMethod.power()[0].equals("")) {
                    System.out.println("方法上 @CheckPower 没有配置任何权限," +
                            "表示不考虑 controller 上是否配置了权限,只要登录就可以任意访问");
                    Object ret = pjp.proceed();
                    return ret;
                } else {
                    //方法上需要验证权限,此时先看【类】上的权限验证是否通过,通过后再考虑验证方法上的权限
                    if (classCheckPowerFlag) {
                        boolean methodCheckPowerFlag =
                                getCheckPowerResult(powerlist, cpMethod.power(), cpMethod.loggic());
                        if (methodCheckPowerFlag) {
                            Object ret = pjp.proceed();
                            return ret;
                        } else {
                            return Result.fail(-1, "没有权限访问");
                        }
                    } else {
                        return Result.fail(-1, "没有权限访问");
                    }
                }
            } else {
                //如果方法上没有 @CheckPower 注解,则判断是否具有访问类的权限
                if (classCheckPowerFlag) {
                    Object ret = pjp.proceed();
                    return ret;
                } else {
                    return Result.fail(-1, "没有权限访问");
                }
            }
        } else {
            //类上面没有注解,则不验证权限,无论是否登录,都可以访问
            Object ret = pjp.proceed();
            return ret;
        }
    }

    //判断用户的权限,是否满足所需要的访问权限
    private Boolean getCheckPowerResult(String[] userPowerList, String[] checkPowerList, String loggic) {
        if (loggic.equalsIgnoreCase("or")) {
            // or 关系,只要用户具有的任意一个权限,在配置的权限列表中,就可以访问
            return CollectionUtil.containsAny(Arrays.asList(userPowerList), Arrays.asList(checkPowerList));
        } else if (loggic.equalsIgnoreCase("and")) {
            // and 关系,要求用户的权限,必须包含所配置的权限列表
            return CollectionUtil.containsAll(Arrays.asList(userPowerList), Arrays.asList(checkPowerList));
        } else {
            return false;
        }
    }
}

默认情况下,SpringBoot 只允许访问 resources 下面的 static 目录下的静态资源,由于使用了 knife4j 接口文档,因此需要对其资源进行放行,否则我们无法访问 knife4j 的接口文档,具体代码如下:

package com.jobs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@EnableOpenApi
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    //设置静态资源目录,以及访问地址映射,这里放行 knife4j 文档的访问地址
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    @Bean
    public Docket createRestApi() {
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.jobs.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("我的测试")
                .version("1.0")
                .description("【注解+AOP+过滤器】的权限控制测试")
                .build();
    }
}

三、用于测试的接口

需要注意的是:本 Demo 在 CheckPowerAspect 对权限验证的逻辑,与上篇博客的验证逻辑有一点小变化

  • 要求在 controller 类上使用 @CheckPower 注解后,才能对其下面的方法使用 @CheckPower 注解控制权限。如果仅仅只是在方法上添加 @CheckPower 注解,是不会对方法进行任何权限控制的。

  • 如果类上配置了 @CheckPower 注解,但是没有配置任何权限参数,则只会验证用户是否登录,不验证类的访问权限。

  • 如果方法上配置 @CheckPower 注解,但是没有配置任何权限参数,则表示只要登录了就可以访问该方法,不考虑 controller 类上是否配置了权限。该验证逻辑主要是为了满足某些特殊场景的需求。

UserController 不需要进行权限控制,所以没有在其类上添加 @CheckPower 注解。其它的 controller 类或方法需要进行权限控制,因此其 controller 类上必须添加 @CheckPower 注解。

UserController 主要实现用户的登录和退出,在过滤器中已经放行,可以随便访问:

package com.jobs.controller;

import com.alibaba.fastjson.JSON;
import com.jobs.common.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@Api(tags = "用户操作相关接口")
@RequestMapping("/user")
@RestController
public class UserController {

    @Value("${user.username}")
    private String username;

    @Value("${user.password}")
    private String passoword;

    //对于以英文逗号分隔的字符串配置,可以自动转换为数组
    @Value("${user.powerlist}")
    private String[] powerlist;


    @ApiOperation("用户登录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "name", value = "用户名", required = true),
            @ApiImplicitParam(name = "pwd", value = "密码", required = true)
    })
    @PostMapping("/login")
    public Result<String> login(String name, String pwd, HttpServletRequest request) {
        if (username.equals(name) && passoword.equals(pwd)) {
            Map<String, Object> userMap = new HashMap<>();
            userMap.put("username", username);
            userMap.put("powerlist", powerlist);
            String json = JSON.toJSONString(userMap);
            request.getSession().setAttribute("user", json);
            return Result.success("登录成功");
        } else {
            return Result.fail(-1, "用户名或密码不正确");
        }
    }

    @ApiOperation("用户退出")
    @PostMapping("/logout")
    public Result<String> logout(HttpServletRequest request) {
        request.getSession().removeAttribute("user");
        return Result.success("退出成功");
    }
}

Test1Controller 用于测试在方法上使用注解配置权限的场景:

package com.jobs.controller;

import com.jobs.annotation.CheckPower;
import com.jobs.common.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CheckPower
@Api(tags = "test1方法权限测试")
@RequestMapping("/test1")
@RestController
public class Test1Controller {

    @ApiOperation("添加订单测试")
    @GetMapping("/addorder")
    //只要用户具有 addorder 权限,就可以访问
    @CheckPower(power = "addorder")
    public Result addorder() {
        return Result.success("hello addorder 访问成功");
    }

    @ApiOperation("删除订单测试")
    @GetMapping("/delorder")
    //用户具有 root 或 delorder 任意一个权限,就可以访问
    //由于 loggic 的默认值就是 or ,所以可以省略不写
    @CheckPower(power = {"root", "delorder"}, loggic = "or")
    public Result delorder() {
        return Result.success("hello delorder 访问成功");
    }

    @ApiOperation("添加用户测试")
    @GetMapping("/adduser")
    //只要用户具有 adduser 权限,就可以访问
    @CheckPower(power = "adduser")
    public Result adduser() {
        return Result.success("hello adduser 访问成功");
    }

    @ApiOperation("删除用户测试")
    @GetMapping("/deluser")
    //用户具有 admin 或者 deluser 的任意权限,就可以访问
    @CheckPower(power = {"admin", "deluser"}, loggic = "and")
    public Result deluser() {
        return Result.success("hello deluser 访问成功");
    }
}

Test2Controller 用于同时测试在类和方法上使用注解配置权限的场景:

package com.jobs.controller;

import com.jobs.annotation.CheckPower;
import com.jobs.common.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//在 controller 类上面添加了权限验证的注解
@CheckPower(power = "root")
@Api(tags = "test2类权限测试")
@RequestMapping("/test2")
@RestController
public class Test2Controller {

    //在该方法上添加了 @CheckPower 注解,但是没有配置任何权限
    //此时即使 controller 上配置了权限,并且验证不通过,该方法也可以不验证权限进行访问
    @CheckPower
    @ApiOperation("查看订单列表")
    @GetMapping("/vieworder")
    public Result vieworder() {
        return Result.success("hello vieworder 访问成功");
    }


    //在方法上没有添加权限验证的注解
    @ApiOperation("查看订单详情")
    @GetMapping("/viewdetail")
    public Result getdetail() {
        return Result.success("hello viewdetail 访问成功");
    }
}

Test3Controller 用于测试在类上使用注解配置访问权限的场景:

package com.jobs.controller;

import com.jobs.annotation.CheckPower;
import com.jobs.common.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//在 controller 类上面添加了权限验证的注解
@CheckPower(power = "admin")
@Api(tags = "test3类上的权限测试")
@RequestMapping("/test3")
@RestController
public class Test3Controller {

    //在方法上没有添加权限验证的注解
    @ApiOperation("查看用户测试")
    @GetMapping("/viewuser")
    public Result viewuser() {
        return Result.success("hello viewuser 访问成功");
    }
}

最后运行 SpringBoot 工程,访问 http://localhost:8888/doc.html 即可查看接口文档,通过其调试功能即可验证:

image

运行效果为:在没有运行成功登录接口之前,访问每个接口都会提示需要登录,登录之后访问相关接口,就能够验证权限。


本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_annotion2.zip