使用【注解】加【拦截器】实现权限控制
前面介绍了使用 SpringSecurity 进行权限控制,其中一个非常方便的特点就是:可以在类和方法上使用注解,从而实现对资源访问的权限控制。但是 Spring Security 具有一定的学习成本和复杂度,想要灵活驾驭并用好框架并非一件容易的事情,比如跟其它系统进行单点登录集成等等。
本篇博客介绍注解加拦截器的自定义权限控制方案,所有代码完全由自己掌控,而且实现了在在类和方法上使用注解实现对资源访问的权限控制。在绝大多数项目中,该方案完全可以满足需求。在本篇博客的最后会提供源代码下载。
需要注意的是:由于本篇博客只是进行 Demo 代码演示,所以使用了 Session 的方案。在实际项目中,可以参考该 Demo 进行方案改造,采用 Token 加 Redis 的方式进行用户信息存储,确保后端服务接口的无状态,从而能够满足负载均衡的需求。
一、搭建工程
搭建一个 SpringBoot 工程,其结构如下所示:
CheckPower 是自定义的注解,用来配置访问资源所需要的权限信息
Result 是自定义的返回结果类,统一使用该类的实例对象生成 json 返回给前端
WebMvcConfig 是对网站相关的配置,包括:拦截器配置、静态资源放行、knife4j 接口文档配置等
controller 下面的类都是对外提供的接口,我们会在这里的类和方法上使用注解配置访问权限
CheckPowerInterceptor 是自定义的拦截器,用于对 controller 类和方法上的注解进行解析,控制用户访问权限
先看一下 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_annotion1</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> <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>
我们不使用相关的 web 页面进行测试,这里引入 knife4j 依赖包,使用接口文档进行接口测试,非常方便。
然后看一下 application.yml 文件内容,我们没有连接数据库去验证用户登录,这里直接配置了测试用户信息:
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
最后再看一下 Result 类,统一使用该类的实例对象生成 json 返回给前端:
package com.jobs.common; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; @ApiModel("返回结果") @Data public class Result<T> implements Serializable { @ApiModelProperty("状态码") private Integer status; @ApiModelProperty("状态消息") private String msg; @ApiModelProperty("返回的数据") 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; } }
二、注解和拦截器
创建自定义注解 CheckPower ,可以配置在 controller 类以及其内部的方法上,其内容如下:
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"; }
power 属性是个数组,可以为资源配置多个访问权限,其默认值是空字符串。如果没有对 power 属性进行配置,power 属性就是拥有一个空字符串元素的数组。
loggic 属性表示 power 数组中配置的多个权限值的验证关系,如果是 and 表示需要用户必须同时拥有 power 数组中配置的所有权限值才可以访问资源,如果是 or 表示只要用户只需要拥有 power 数组中任意一个权限值就可以访问资源。
下面列出拦截器的内容,其功能就是解析 controller 以及其内部的方法上的 CheckPower 注解,通过比对用户本身的权限,判断用户是否满足资源所配置的权限,如果不满足则直接将没有访问权限的提示信息返回给前端。
package com.jobs.interceptor; 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.Result; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; //执行顺序:过滤器 > 拦截器 > AOP //本 Demo 只使用了拦截器,进行 controloler 和 method 上的 @CheckPower 注解解析和权限判断 //本 Demo 没有使用到【过滤器】和【AOP】 @Component public class CheckPowerInterceptor implements HandlerInterceptor { //拦截请求,在请求执行前,执行该方法, //这里判断 ontrololer 和 method 上是否有 @CheckPower 注解, //如果有的话,则判断用户的权限,是否满足所设置的执行权限 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果 session 存在,则表明已经登录过了 Object user = request.getSession().getAttribute("user"); if (user == null) { //用户需要登录 ResposeResult(response, Result.fail(-99, "请登录后再访问")); return false; } if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; //获取用户具有的权限 JSONObject userMap = JSON.parseObject(user.toString()); JSONArray jsonArray = userMap.getJSONArray("powerlist"); String[] powerlist = jsonArray.toArray(new String[jsonArray.size()]); // controller 上配置的权限是否满足,默认为 true 表示满足 boolean controllerCheckPowerFlag = true; //获取类上的注解,看是否存在 CheckPower 注解 CheckPower cpClass = hm.getBeanType().getAnnotation(CheckPower.class); if (cpClass != null) { System.out.println("访问的 Controller 上有 @CheckPower 注解..."); //获取配置的权限列表 System.out.println("Controller 需要的权限为:" + Arrays.toString(cpClass.power()) + ",权限逻辑关系:" + cpClass.loggic()); System.out.println("用户具有的权限:" + Arrays.toString(powerlist)); //如果 controller 上有 @CheckPower 注解,并且没有配置任何权限,则表示登录后可以随便访问 if (cpClass.power().length == 1 && cpClass.power()[0].equals("")) { controllerCheckPowerFlag = true; } else { controllerCheckPowerFlag = getCheckPowerResult(powerlist, cpClass.power(), cpClass.loggic()); } } //先判断方法上是否有 @CheckPower 注解 if (hm.hasMethodAnnotation(CheckPower.class)) { System.out.println("访问的 Method 方法上有 @CheckPower 注解..."); CheckPower cpMethod = hm.getMethodAnnotation(CheckPower.class); //获取配置的权限列表 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 上是否配置了权限,只要登录就可以任意访问"); return true; } //方法上需要验证权限,此时先看 controler 上的权限验证是否通过,通过后再考虑验证方法上的权限 if (controllerCheckPowerFlag) { boolean methodCheckPowerFlag = getCheckPowerResult(powerlist, cpMethod.power(), cpMethod.loggic()); if (methodCheckPowerFlag == false) { ResposeResult(response, Result.fail(-1, "没有权限访问")); } return methodCheckPowerFlag; } } if (controllerCheckPowerFlag == false) { ResposeResult(response, Result.fail(-1, "没有权限访问")); } return controllerCheckPowerFlag; } return true; } //判断用户的权限,是否满足所需要的访问权限 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; } } //返回给前端 json 结果 private void ResposeResult(HttpServletResponse response, Result result) throws IOException { response.setContentType("application/json;charset=utf-8"); String json = JSON.toJSONString(result); response.getWriter().write(json); } }
这里也实现了一个逻辑:如果 controller 和其内部的一个方法资源(假设方法名称为 aaa)上,同时配置了 CheckPower 注解权限,此时如果方法(aaa)上配置的 CheckPower 权限列表为空,则不再考虑 controller 上配置的权限,用户只要登录了,就可以访问方法(aaa)。之所以实现这样的权限控制逻辑,主要是为了满足这样的场景:controller 中的绝大多数方法需要控制权限,只需要在 controller 类上加上注解即可,但是个别方法不需要控制权限,只需要为这些方法增加 @CheckPower 注解,但不进行 power 属性的配置即可。
最后我们需要对拦截器进行相关配置,以便其能够在 SpringBoot 中生效,拦截所有的请求,但是要放行一些资源,比如用户登录接口等等,具体要放行的资源,大家可以抽取出来,配置到 yml 文件中,这里为了方便就直接在代码中写死了。
package com.jobs.config; import com.jobs.interceptor.CheckPowerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 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/"); } //拦截器,拦截所有请求,一方面判断用户是否登录, //另一方面判断类上是否有 @CheckPower 注解,如果有则判断当前登录的用户是否有权限 @Override protected void addInterceptors(InterceptorRegistry registry) { //添加拦截器(可以添加多个拦截器,拦截器的执行顺序,就是添加顺序) CheckPowerInterceptor myInterceptor = new CheckPowerInterceptor(); //设置拦截器拦截的请求路径,以及不拦截的请求路径 registry.addInterceptor(myInterceptor).addPathPatterns("/**") .excludePathPatterns(getExcludePathPatterns()); } //在这里设置拦截器不需要进行拦截的路径 private String[] getExcludePathPatterns() { String[] uris = new String[]{ //放行用户登录接口 "/user/login", //放行用户退出接口 "/user/logout", //放行下面的 knifefj 的静态资源文件路径 "/doc.html", "/webjars/**", "/swagger-resources", "/v2/api-docs" }; return uris; } @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("注解加拦截器的权限控制测试") .build(); } }
三、用于测试的接口
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.*; @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; @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
即可查看接口文档,通过其调试功能即可验证:
运行效果为:在没有运行成功登录接口之前,访问每个接口都会提示需要登录,登录之后访问相关接口,就能够验证权限。
本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_annotion1.zip
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?