java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot + JWT + RBAC 权限系统实战

基于SpringBoot、SpringSecurity、JWT、RBAC搭建一套可落地的权限系统

作者:百锦再@新空间创想科技

本文详解释如何基于SpringBoot、SpringSecurity、JWT、RBAC搭建一套可落地的权限系统,文章指出项目中常见的权限混乱问题,介绍了JWT在权限系统中的的角色,以及RBAC模型的优势,阐述了权限系统需要解决的核心问题,如确认用户身份、确认用户权限、后端接口安全边界等

前言

在企业级后台系统开发中,登录认证、接口鉴权、角色管理、权限控制几乎是绕不过去的核心基础能力。

很多项目初期只是简单做了一个登录接口,等业务逐渐复杂之后,就会暴露出一系列问题:

这篇文章就从实际项目角度出发,基于 Spring Boot + Spring Security + JWT + RBAC,带你完整搭建一套可落地的权限系统。

本文主要覆盖以下内容:

一、权限系统为什么容易越做越乱

权限系统做乱,往往不是因为不会写代码,而是从建模开始就有问题。

常见错误主要有以下几类。

1. 角色和权限混为一谈

比如代码里直接写死:

一开始角色少的时候还能勉强维护,但随着角色越来越多,代码里会充满各种 if else,后续每新增一个角色,业务判断都会继续膨胀。

2. 只做前端权限,不做后端权限

很多项目把“权限控制”理解成:

这只能改善交互体验,不能形成真正的安全边界。只要知道接口地址,攻击者或者越权用户依然可能直接发请求访问接口。

3. 权限粒度设计不清晰

有的系统权限点是中文,有的是 URL,有的是动作名,最后会变成这种情况:

这种混乱命名一旦出现,权限表、代码注解、前端判断就会越来越难维护。

4. 登录认证和权限授权耦合严重

很多项目登录后返回一堆角色类型,后端再根据角色做硬编码判断,前端也根据角色做页面渲染。角色变更一次,就要同时改多个地方。

5. JWT 用了,但没有打通权限模型

很多系统确实使用了 JWT,但只是为了无状态登录。登录成功之后,token 里只有用户 ID,接口校验也只是“是否登录”,并没有真正校验当前用户是否具备某项权限。

所以权限系统的关键,不是用了什么框架,而是是否把“认证”和“授权”分开设计,并且是否真正落实到了后端接口层。

二、权限系统要解决的核心问题

权限系统的目标不是“用户能登录”,而是要真正解决下面几个问题。

1. 确认当前用户是谁

也就是认证,Authentication。

用户通过用户名密码、短信验证码、第三方登录等方式登录之后,系统需要确认身份。

2. 确认当前用户能做什么

也就是授权,Authorization。

登录成功只代表身份合法,不代表可以访问所有页面、所有菜单和所有接口。

3. 接口必须有真正的安全边界

菜单隐藏不是权限控制的终点,真正的边界一定是后端接口。

4. 支持后续角色和权限扩展

一个小系统可能一开始只有管理员和普通用户,但后面可能会扩展出:

如果模型一开始设计得过于简单,后期扩展成本会非常高。

5. 前后端规则要一致

后端负责最终权限校验,前端负责菜单、按钮、页面的动态控制,两者必须基于同一套权限点设计。

三、为什么大多数后台系统都适合用 RBAC

RBAC 的全称是 Role-Based Access Control,也就是基于角色的访问控制。

它最核心的关系链是:

用户 -> 角色 -> 权限

这种模型的好处很明显:

举个简单例子。

系统里有三个角色:

系统管理员拥有全部权限;
咨询师可以查看用户信息、管理咨询记录;
普通用户只能查看自己的资料和订单。

如果没有角色层,就需要给每个用户单独分配权限,维护成本会迅速失控。

RBAC 的价值就在于把“用户”和“能力”解耦了。

四、权限点应该怎么设计

权限点设计建议采用统一命名规则:

模块:资源:动作

例如:

sys:user:view
sys:user:create
sys:user:update
sys:user:delete
sys:role:view
sys:role:assign
order:refund:approve

这样设计有几个明显优势。

1. 语义清晰

看到权限码就能知道它控制的是什么能力。

2. 前后端都能共用

前端按钮、菜单、路由守卫和后端 @PreAuthorize 可以使用同一套权限码。

3. 适合数据库持久化

权限表里直接维护 permission_code,便于检索、比对和扩展。

4. 便于接口级权限控制

Spring Security 中可以直接写:

@PreAuthorize("hasAuthority('sys:user:view')")

这里要强调一个原则:

不要把 URL 当权限码,也不要把中文文案当权限码。

权限码应该是稳定、统一、可维护的系统内部标识。

五、JWT 在权限系统中的作用

JWT 本质上是一种令牌机制,用来在前后端之间传递登录态。

一个典型的 JWT 包括三部分:

在权限系统中,JWT 主要承担的是“认证凭证”的角色,而不是权限系统本身。

JWT 的典型使用流程如下:

  1. 用户提交用户名和密码登录
  2. 后端验证通过后生成 JWT
  3. 前端保存 token
  4. 后续请求在请求头中携带:
Authorization: Bearer xxx
  1. 后端通过过滤器解析 token,确认当前用户身份
  2. 结合权限信息完成授权判断

JWT 的优势:

但 JWT 也有明显问题:

因此,JWT 只是权限体系中的一环,而不是全部。

六、Spring Boot + JWT + Spring Security 的整体实现流程

一个完整的权限系统实现流程一般如下:

第一步:用户登录

前端调用登录接口,提交用户名和密码。

第二步:服务端校验身份

后端校验用户名密码是否正确。

第三步:查询用户角色和权限

根据当前用户查出其角色集合和权限集合。

第四步:签发 JWT

后端把用户身份信息和必要权限信息写入 token,返回给前端。

第五步:前端保存 token

后续请求统一通过请求头携带 token。

第六步:后端过滤器解析 token

JWT 过滤器提取用户名、权限,并构造 Spring Security 的认证上下文。

第七步:接口做权限校验

Controller 层通过 @PreAuthorize 校验权限,决定能否执行接口。

这套流程里,最关键的是两部分:

七、权限系统的数据模型怎么设计

最基础的 RBAC 表设计通常包括下面几张表:

1. 用户表sys_user

保存用户名、密码、昵称、状态、创建时间等基础信息。

2. 角色表sys_role

保存角色编码、角色名称、状态等信息。

3. 权限表sys_permission

保存权限编码、权限名称、类型等。

4. 用户角色关联表sys_user_role

描述用户和角色之间的多对多关系。

5. 角色权限关联表sys_role_permission

描述角色和权限之间的多对多关系。

如果还需要控制菜单,可以继续增加菜单表:

6. 菜单表sys_menu

菜单项可关联 permission_code,前端根据权限动态渲染。

注意一点:

菜单不是权限,菜单只是权限的展示入口。

真正的权限控制最终还是要落到接口上。

八、登录接口设计

登录接口的请求参数一般比较简单:

public record LoginRequest(String username, String password) {
}

返回值建议包含三类信息:

例如:

public record LoginResponse(
        String token,
        UserInfo user,
        Set<String> permissions
) {
    public record UserInfo(Long id, String username, String nickname, Set<String> roles) {
    }
}

登录服务逻辑示例:

@Service
public class AuthService {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;

    public LoginResponse login(LoginRequest request) {
        User user = userService.findByUsername(request.username())
                .orElseThrow(() -> new IllegalArgumentException("用户名或密码错误"));

        if (!passwordEncoder.matches(request.password(), user.getPassword())) {
            throw new IllegalArgumentException("用户名或密码错误");
        }

        Set<String> permissions = userService.getPermissions(user.getId());
        String token = jwtTokenProvider.generateToken(user.getUsername(), permissions);

        return new LoginResponse(
                token,
                new LoginResponse.UserInfo(user.getId(), user.getUsername(), user.getNickname(), user.getRoles()),
                permissions
        );
    }
}

登录接口设计要点

九、JWT 工具类实现

JWT 工具类主要负责三件事:

示例代码如下:

@Component
public class JwtTokenProvider {

    private final SecretKey secretKey;
    private final long expiration;

    public JwtTokenProvider(@Value("${security.jwt.secret}") String secret,
                            @Value("${security.jwt.expiration}") long expiration) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.expiration = expiration;
    }

    public String generateToken(String username, Set<String> permissions) {
        Date now = new Date();
        Date expireTime = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .subject(username)
                .claim("permissions", permissions)
                .issuedAt(now)
                .expiration(expireTime)
                .signWith(secretKey)
                .compact();
    }

    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public String getUsername(String token) {
        return parseToken(token).getSubject();
    }

    public List<String> getPermissions(String token) {
        return parseToken(token).get("permissions", List.class);
    }
}

实际项目中的注意点

十、JWT 过滤器接入 Spring Security

为了让 Spring Security 知道当前请求对应的是哪个用户,需要在请求进入 Controller 之前先经过 JWT 过滤器。

示例代码如下:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);

            try {
                String username = jwtTokenProvider.getUsername(token);
                List<String> permissions = jwtTokenProvider.getPermissions(token);

                List<GrantedAuthority> authorities = permissions.stream()
                        .map(SimpleGrantedAuthority::new)
                        .toList();

                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(username, null, authorities);

                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (Exception ignored) {
            }
        }

        filterChain.doFilter(request, response);
    }
}

这个过滤器的本质就是:

后续接口层的权限校验就能基于这个上下文进行。

十一、Spring Security 配置

核心配置通常包括以下几件事:

示例代码如下:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

这里的核心原则是:

登录接口放行,其他接口默认先要求登录,再在具体接口方法上细分权限。

十二、接口级权限控制为什么推荐@PreAuthorize

相比把所有权限规则都堆在过滤器里,把权限写在接口方法上更清晰、更可维护。

示例:

@PreAuthorize("hasAuthority('sys:user:view')")
@GetMapping("/api/users")
public ApiResponse<List<UserResponse>> list() {
    return ApiResponse.ok(userService.findAll());
}

再比如新增用户:

@PreAuthorize("hasAuthority('sys:user:create')")
@PostMapping("/api/users")
public ApiResponse<UserResponse> create(@RequestBody @Valid UserRequest request) {
    return ApiResponse.ok(userService.create(request));
}

这种方式的优点很明确:

例如:

@PreAuthorize("hasAuthority('sys:role:view') or hasAuthority('sys:menu:view')")

十三、RESTful 用户管理接口设计

用户管理接口建议遵循 RESTful 风格设计:

示例代码如下:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PreAuthorize("hasAuthority('sys:user:view')")
    @GetMapping
    public ApiResponse<List<UserResponse>> list() {
        return ApiResponse.ok(userService.findAll());
    }

    @PreAuthorize("hasAuthority('sys:user:view')")
    @GetMapping("/{id}")
    public ApiResponse<UserResponse> detail(@PathVariable Long id) {
        return ApiResponse.ok(userService.findById(id));
    }

    @PreAuthorize("hasAuthority('sys:user:create')")
    @PostMapping
    public ApiResponse<UserResponse> create(@RequestBody @Valid UserRequest request) {
        return ApiResponse.ok(userService.create(request));
    }

    @PreAuthorize("hasAuthority('sys:user:update')")
    @PutMapping("/{id}")
    public ApiResponse<UserResponse> update(@PathVariable Long id,
                                            @RequestBody @Valid UserRequest request) {
        return ApiResponse.ok(userService.update(id, request));
    }

    @PreAuthorize("hasAuthority('sys:user:delete')")
    @DeleteMapping("/{id}")
    public ApiResponse<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ApiResponse.ok(null);
    }
}

不要把接口写成:

这种动作式路径不利于规范统一,也不利于接口文档维护。

十四、统一返回结构的必要性

很多项目会对返回值做统一包装,例如:

public record ApiResponse<T>(boolean success, String message, T data) {
    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(true, "success", data);
    }

    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(false, message, null);
    }
}

它的优点是:

但也要注意:

统一返回结构不等于可以忽略 HTTP 状态码。

例如:

HTTP 状态码和业务响应体应该一起使用,而不是互相替代。

十五、Swagger 为什么应该尽早接入

权限系统类项目接口很多,调试频率也很高,所以 Swagger 文档很有必要。

优势主要有三个:

Spring Boot 中可以使用 springdoc-openapi,配置完成后访问:

/swagger-ui.html

即可查看接口文档。

如果接口需要 Bearer Token,还可以在 OpenAPI 中配置安全方案,让 Swagger 页面直接支持携带 token 测试受保护接口。

十六、前后端权限协同怎么做

前端和后端的职责一定要分清楚:

后端职责

前端职责

一个典型做法是:

登录成功后,后端返回:

然后前端再调用一个权限元数据接口,例如:

GET /api/permissions/meta

返回:

前端根据权限集合判断:

但需要再次强调:

前端的权限判断只能改善体验,真正的安全校验必须由后端完成。

十七、权限系统常见坑点

这一部分是最容易踩坑的地方。

1. 只做菜单权限,不做接口权限

这不是权限系统,只是 UI 隐藏。

2. 把角色判断写死在业务代码里

例如:

if ("admin".equals(role)) {
    ...
}

这种方式在角色扩展后会迅速失控。

3. 权限码不统一

同一个系统里混用多种命名风格,后续维护会非常痛苦。

4. 把全部权限都塞进 JWT

权限一旦变更,旧 token 中的权限快照可能仍然有效。

5. 没有 token 失效策略

比如用户被禁用、修改密码、角色调整后,旧 token 依然可用。

6. 没有区分功能权限和数据权限

能访问用户列表,不代表能访问所有用户数据。

十八、数据权限怎么扩展

当系统除了控制“功能能不能访问”,还要控制“数据能不能看到”时,就进入了数据权限层。

典型数据范围包括:

实现方式一般有两类:

1. 在 Service 层加过滤条件

例如只能查当前用户自己创建的数据:

creator_id = currentUserId

2. 在持久层统一注入数据范围条件

例如 MyBatis 拦截器、SQL 拼接等。

这里要明确一点:

接口权限和数据权限不是一回事。

你可能有权限访问某个接口,但返回数据范围仍然需要继续过滤。

十九、JWT 失效与刷新策略

JWT 常见的两个问题是:

一个较成熟的方案是双 token:

access token 用于访问接口,refresh token 用于换发新 token。

如果系统规模不大,也可以先做简化版:

核心不是一步做到最复杂,而是设计上要预留升级空间。

二十、权限系统为什么需要审计日志

权限系统如果没有审计能力,后期排查问题会非常被动。

建议至少记录以下行为:

审计日志建议包含:

一旦出现越权、误删、误授权,这些信息就是最直接的排查依据。

二十一、一个建议的落地路径

如果你要从零搭建权限系统,建议按阶段推进。

第一阶段:最小可用版

第二阶段:管理能力补齐

第三阶段:安全增强

第四阶段:复杂场景扩展

这样做的好处是每一阶段都可以形成明确交付,而不是一开始就做成一个难以落地的大系统。

总结

权限系统是后台项目中非常基础、但也非常容易被做偏的一部分。

真正可落地的权限系统,至少要满足下面几个原则:

从工程实践角度看,Spring Boot + Spring Security + JWT + RBAC 是一套足够成熟且稳定的方案。只要权限模型清晰、接口层保护到位、前后端协同一致,这套方案完全可以支撑大多数企业后台系统。

参考代码片段汇总

登录接口权限流程

@PostMapping("/api/auth/login")
public ApiResponse<LoginResponse> login(@RequestBody LoginRequest request) {
    return ApiResponse.ok(authService.login(request));
}

接口级权限控制

@PreAuthorize("hasAuthority('sys:user:view')")
@GetMapping("/api/users")
public ApiResponse<List<UserResponse>> list() {
    return ApiResponse.ok(userService.findAll());
}

JWT 过滤器

String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) {
    String token = header.substring(7);
}

 

到此这篇关于基于SpringBoot、SpringSecurity、JWT、RBAC搭建一套可落地的权限系统的文章就介绍到这了,更多相关Spring Boot + JWT + RBAC 权限系统实战内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文