导读:本期聚焦于落伍者创作的《在Java中如何实现简单权限校验逻辑_Java权限控制项目讲解》,敬请观看详情。权限校验是几乎所有后台系统都绕不开的功能,没有权限控制的接口就像不上锁的门,任何登录用户都能随意操作数据。本文以一个简单的Java Web项目为例,从权限模型设计讲起,介绍用户、角色、权限三张表的关联关系,再通过自定义注解配合Spring拦截器实现接口级别的权限校验,同时也提供基于过滤器和手动判断的轻量方案作为对比。文章包含完整可运行的代码示例,讲解注解的定义与解析、拦截器的注册流程,以及权限校验失败时统一异常处理的写法,适合正在做课程设计或入门级管理系统的开发者参考。

做后台管理系统时,权限校验几乎是绕不开的一环。想象一个场景:普通用户通过修改浏览器地址栏里的URL,直接访问到了管理员的删除接口,数据瞬间被清空。这类事故的根源就是接口没有做权限控制。本文将用Java实现一套简单但完整的权限校验逻辑,覆盖权限模型设计、注解定义、拦截器实现和异常处理,代码可以直接跑在Spring Boot项目中。

在Java中如何实现简单权限校验逻辑_Java权限控制项目讲解

权限模型设计:用户、角色、权限三张表

经典的权限模型是RBAC(基于角色的访问控制),核心思路是用户不直接持有权限,而是通过角色间接获得。一个用户可以拥有多个角色,一个角色可以包含多个权限,这样就形成了用户表、角色表、权限表,再加两张关联表的结构。

设计成三张表的好处是维护成本低。比如财务部有20个人,当他们的权限需要调整时,只需要修改“财务角色”的权限集合,20个用户自动生效,不用逐个修改用户记录。如果让用户直接绑定权限,每次调整都是一场灾难。

简化的表结构如下,实际项目中可以按需扩展字段:

-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(100) NOT NULL
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_code VARCHAR(50) NOT NULL COMMENT '角色编码,如 ADMIN'
);

-- 用户角色关联表
CREATE TABLE sys_user_role (
    user_id BIGINT,
    role_id BIGINT
);

-- 权限表
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    perm_code VARCHAR(100) NOT NULL COMMENT '权限编码,如 user:delete'
);

-- 角色权限关联表
CREATE TABLE sys_role_permission (
    role_id BIGINT,
    permission_id BIGINT
);

权限编码建议采用“模块:操作”的命名方式,比如user:deleteorder:export,一眼就能看出这个权限控制的是什么资源,后期排查问题时也方便在代码里全局搜索。

自定义注解:让权限声明写在接口上

权限校验的第一步是让每个接口能声明自己需要什么权限。最优雅的方式是自定义一个注解,比如@RequirePermission,然后直接标注在Controller方法上。这样接口需要的权限一目了然,比在代码里写一堆if判断干净得多。

注解的定义非常简单,注意要指定RetentionPolicy.RUNTIME,否则运行期通过反射拿不到注解信息:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    /**
     * 需要的权限编码,支持传多个,默认只要满足其中一个即可
     */
    String[] value();

    /**
     * 多个权限之间的关系,AND表示全部满足,OR表示满足其一即可
     */
    Logical logical() default Logical.OR;
}

enum Logical {
    AND, OR
}

使用的时候只需要一行代码,Controller保持清爽:

@RestController
@RequestMapping("/user")
public class UserController {

    @RequirePermission("user:delete")
    @DeleteMapping("/{id}")
    public Result deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return Result.success();
    }

    // 需要同时拥有两个权限才能操作
    @RequirePermission(value = {"order:export", "order:view"}, logical = Logical.AND)
    @GetMapping("/export")
    public Result exportOrders() {
        return Result.success(orderService.export());
    }
}

如果整个类都需要同一套权限,也可以在@Target里加上ElementType.TYPE,让注解支持标注在类上,拦截器解析时先查方法上的注解,没有再查类上的注解,实现继承效果。

拦截器实现:核心校验逻辑落地

注解只是声明,真正干活的是拦截器。拦截器会在请求进入Controller之前执行,我们从请求映射到的方法上反射读取@RequirePermission注解,然后对比当前登录用户拥有的权限集合,不满足就直接拒绝。

完整实现如下,假设登录后用户权限已经放入Token或Session中:

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Set;

public class PermissionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 静态资源等非Controller请求直接放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        RequirePermission annotation =
                handlerMethod.getMethodAnnotation(RequirePermission.class);

        // 没有注解说明接口不需要权限,放行
        if (annotation == null) {
            return true;
        }

        // 从会话中取出当前用户的权限集合(登录时写入)
        Set<String> userPerms =
                (Set<String>) request.getSession().getAttribute("userPerms");

        if (userPerms == null || userPerms.isEmpty()) {
            response.setStatus(401);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"msg\":\"未登录或权限为空\"}");
            return false;
        }

        String[] required = annotation.value();
        boolean pass;
        if (annotation.logical() == Logical.AND) {
            // 全部满足
            pass = userPerms.containsAll(Arrays.asList(required));
        } else {
            // 满足其一即可
            pass = Arrays.stream(required).anyMatch(userPerms::contains);
        }

        if (!pass) {
            response.setStatus(403);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}");
        }
        return pass;
    }
}

写完拦截器别忘了注册,否则Spring根本不知道它的存在。注册代码要放在配置类中,并配置排除路径,比如登录接口本身就不能被拦截:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermissionInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/register", "/error");
    }
}

有一点需要特别注意:拦截器抛出的异常和直接写回JSON响应是两种风格,上面代码选择直接写响应是为了演示简单,规范一点的做法是抛出自定义的PermissionDeniedException,再通过全局异常处理器统一返回,这样前端拿到的错误格式才和业务异常保持一致。

统一异常处理与登录时加载权限

权限数据应该在登录成功后一次性查出来放入会话,而不是每次请求都查数据库。登录逻辑大致是:校验用户名密码,查询用户的所有角色,再汇总所有角色的权限编码,去重后存入Session。

@Service
public class LoginService {

    @Autowired
    private UserMapper userMapper;

    public void login(String username, String password, HttpSession session) {
        SysUser user = userMapper.selectByUsername(username);
        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
            throw new BizException("用户名或密码错误");
        }
        // 一次性查出该用户的所有权限编码
        Set<String> perms = userMapper.selectPermissionsByUserId(user.getId());
        session.setAttribute("userPerms", perms);
    }
}

对应的SQL用三表联查加上DISTINCT去重:

SELECT DISTINCT p.perm_code
FROM sys_user_role ur
JOIN sys_role_permission rp ON ur.role_id = rp.role_id
JOIN sys_permission p ON rp.permission_id = p.id
WHERE ur.user_id = #{userId}

再配一个全局异常处理器,把403场景的返回结构统一掉:

import org.springframework.web.bind.annotation.*;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(PermissionDeniedException.class)
    public Result handlePermissionDenied(PermissionDeniedException e) {
        return Result.fail(403, "权限不足:" + e.getMessage());
    }
}

方案对比与注意事项

除了拦截器方案,还有两种常见做法。一种是Servlet Filter,它执行时机比拦截器更早,连静态资源请求都会经过,适合做登录状态校验这类粗粒度控制,但Filter里拿不到HandlerMethod,无法方便地读取方法上的注解,做接口级权限比较别扭。另一种是AOP切面,功能上和拦截器类似,也能解析注解,适合需要在权限校验前后附加逻辑的场景,比如记录操作日志。对于普通单体项目,拦截器是最平衡的选择。

几个容易踩的坑:一是管理员后台修改了角色权限后,已登录用户的Session里还是旧权限,需要在改权限时清掉相关用户的会话,或者把权限缓存到Redis并设置合理过期时间;二是注解只写在了类上没写在方法上时,记得用AnnotatedElementUtils.findMergedAnnotation处理合并注解;三是前后端分离项目用Token替代Session后,拦截器里改为从请求头解析Token再查Redis即可,整体结构不用变。

本文实现的权限校验逻辑不到两百行代码,但覆盖了模型设计、注解声明、拦截执行、异常兜底的完整链路,稍加扩展就能支持按钮级权限和数据权限,足够应付大部分课程设计和中小型管理系统的需求。

Java权限校验权限控制拦截器修改时间:2026-09-08 00:00:39

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52508.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。