掘金 后端 ( ) • 2024-04-01 11:29

SpringBoot中使用自定义注解和拦截器实现简单的权限控制

本文首发在我的个人站点:追逐日落,欢迎大家前去参观~

前言

众所周知,作为系统的最后一道防线,对于一些重要操作(crud等),后端默认接收的所有的请求都是“不可信”的,除了确认用户登录状态之外,还要对接收的数据进行一系列的合法性校验等等,即使已经在前端页面对用户输入信息做了一定的限制。因为请求可以伪造,也可能被拦截篡改,即使在正常情况下,用户也可能因为误操作或者恶意行为发送不合法的请求。

如果多角色系统的接口不做权限校验的话,那无疑是在“裸奔”,任何一个普通的登录用户都能调用所有的接口。

Q:什么是权限控制?

A:让特定的用户只能访问特定的资源。

在实际项目中,如果权限控制需要复杂的逻辑或者需要非常细致的权限划分,可能需要借助专门的权限框架,主流的权限框架有

  • Spring Security https://spring.io/projects/spring-security

    Spring Security 是一个功能强大、高度可定制的身份验证和访问控制框架。它是确保基于 Spring 的应用程序安全的事实标准。 Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正威力在于它可以轻松扩展以满足自定义需求

  • Apache Shiro https://shiro.apache.org/

    Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。利用 Shiro 易于理解的 API,您可以快速、轻松地保护任何应用程序的安全--从最小的移动应用程序到最大的网络和企业应用程序。

虽然上述的框架功能十分完备,但是对于只有两三个角色的系统来说,杀鸡焉用宰牛刀啊,通过使用SpringBoot框架提供的自定义注解和拦截器功能,可以轻松地实现对不同用户角色的访问权限控制。

本文用例说明:

系统有三种角色,管理员、教师、学生。

在数据库用户表中,用户类型字段以tinyint存储,3代表管理员,2代表教师,1代表学生。

功能需求:有些接口只能管理员访问,有些接口可以让管理员和老师访问,有些接口只能学生访问。

定义权限常量

在项目下新建constant包,并新建接口用于定义角色权限常量

public interface UserConstant {
    int USER_TYPE_ADMIN = 3; // 管理员
    int USER_TYPE_TEACHER = 2; // 教师
    int USER_TYPE_STUDENT = 1; // 学生
    int USER_TYPE_LOGIN = 0; // 登录未认证身份用户
}

定义Access注解

新建annotation包,在包下新建一个Access注解,用于权限控制

import java.lang.annotation.*;
​
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Access {
    int[] roles() default {};
}

新建权限拦截器

/**
 * 权限拦截器
 */
@Component
public class AccessInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Access access = method.getAnnotation(Access.class);
        //如果role是空的,说明不需要权限,直接放行
        if (access.roles().length == 0) {
            log.info("access " + method.getName() + "role是空的");
            return true;
        }
        //如果是指定角色(们)才能访问的权限
        //获取登录用户信息的方法
        User user = ......<补全>.......;
        //如果用户类型在权限数组中,说明权限足够
        if (Arrays.asList(access.roles()).contains(user.getType())) {
            return true;
        } else {
            response.setStatus(403);
            return false;
        }
    }
}

注册拦截器

将权限拦截器添加到登录拦截器后面,拦截器将会在请求处理过程中按照它们被添加的顺序依次执行。

@Configuration
public class RequestInterceptor implements WebMvcConfigurer {
    
    @Resource
    private AccessInterceptor accessInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        // registry.addInterceptor(loginInterceptor);
        registry.addInterceptor(accessInterceptor);
    }
}

在controller方法上加注解

在需要进行权限校验的接口方法上使用@Access,在注解数组中填入之前定义的常量即可,例如下面的例子表示只有学生用户和管理员用户才可以访问接口。如果没有标记权限注解,或者注解中数组为空,则不会验证该接口请求的权限。

@GetMapping("/hello")
@Access(roles = {UserConstant.USER_TYPE_STUDENT,UserConstant.USER_TYPE_ADMIN})
public BaseResponse<List<Activity>> hello(){
    //统一返回结果类,返回统一格式的响应
    return ResultUtil.success(activityService.list());
}

以上实现了一种在注解中枚举角色的权限控制方法,可以实现简单而灵活的权限控制,保护我们的应用程序免受未经授权的访问。这样使用数组定义注解,好处是自定义程度高,可以指定哪(几)个角色,从而更加精细的控制访问权限,缺点是用户角色较多时,注解添加会比较麻烦。

如果权限划分是分层级的话(例如:超级无敌管理员>超级管理员>普通管理员,高权限用户可以向下访问低权限用户的所有接口,低权限用户不能向上访问),可以将注解里的roles改成整形,并修改权限拦截器,将判定条件修改成比较权限值的大小,只要用户的权限的数值高于接口权限就放行。