CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-224 · JHipster 项目开发

JHipster 开发 09:安全体系(下)——授权

上一篇解决了”你是谁”,这篇解决”你能干什么”。JHipster 的授权体系建立在 Spring Security 的标准模型上,前后端各有一套守卫逻辑,且共用同一份权限清单。理解了数据模型和两道防线,你就能精确控制”谁能看哪个页面、谁能调哪个接口”。

授权模型:用户—角色—权限

JHipster 生成的用户体系是三层结构(表前缀 jhi_):

erDiagram
  JHI_USER ||--o{ JHI_USER_AUTHORITY : ""
  JHI_AUTHORITY ||--o{ JHI_USER_AUTHORITY : ""
  JHI_USER {
    bigint id PK
    varchar login UK
    varchar password_hash
    boolean activated
    varchar lang_key
  }
  JHI_AUTHORITY {
    varchar name PK
  }
  JHI_USER_AUTHORITY {
    bigint user_id FK
    varchar authority_name FK
  }

初始数据两个角色:ROLE_ADMINROLE_USER(存在 jhi_authority 表,所以这张表常被直接称为 authorities 表)。角色字符串即 Spring Security 的 GrantedAuthority 文本——JHipster 约定带 ROLE_ 前缀的可用于 hasRole(),不带前缀的当作细粒度权限用于 hasAuthority()

这个设计留了扩展缝:权限(authority)本质只是一串文本,往表里插一行自定义权限、挂给用户,代码里立刻可用——文末的完整流程就靠这个机制。

后端从库里加载权限的链路:登录/校验时 UserDetailsServicejhi_user 及其 authority 集合(JWT 场景则从 token 的 auth claim 还原,见第 8 篇),填进 Authentication.authorities。之后所有授权判断都在问一个集合。

第一道防线:SecurityConfiguration

生成在 config/SecurityConfiguration.java,职责是组装过滤器链与 URL 级规则。结构化地读它:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)   // 开启方法级注解
public class SecurityConfiguration {

  private final JwtAuthenticationFilter jwtAuthenticationFilter;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.disable())                          // 无状态 JWT 下禁用(API 场景)
      .authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/authenticate").permitAll()
        .requestMatchers("/api/register").permitAll()
        .requestMatchers("/api/activate").permitAll()
        .requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
        .requestMatchers("/api/**").authenticated()
        .requestMatchers("/management/health/**").permitAll()
        .requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
        .requestMatchers("/v3/api-docs/**").permitAll()
      )
      .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }
}

要点解读:

  • URL 级规则挡粗粒度/api/admin/** 要求 ADMIN,/api/** 全部认证,白名单(登录、注册、健康检查、API 文档)显式放行——matchers 的顺序有语义,先具体后宽泛;
  • STATELESS:不开会话,身份完全靠第 8 篇的过滤器每请求重建;
  • @EnableMethodSecurity 才是主战场开关:URL 规则只兜底,JHipster 的授权主力是方法级注解。

URL 级可以改,但我们团队的纪律是:默认规则不动,业务权限一律下沉到方法级。URL 通配改多了容易失控,方法注解跟着代码走、可 review。

第二道防线:方法级 @PreAuthorize

生成的每个 REST 资源类都自带示例(admin 实体的接口):

@RestController
@RequestMapping("/api/admin")
public class UserResource {

  @PutMapping("/users")
  @PreAuthorize("hasAuthority('" + AuthoritiesConstants.ADMIN + "')")
  public ResponseEntity<AdminUserDTO> updateUser(@RequestBody AdminUserDTO userDTO) { … }

  @GetMapping("/users")
  @PreAuthorize("hasAuthority('" + AuthoritiesConstants.ADMIN + "')")
  public ResponseEntity<List<AdminUserDTO>> getAllUsers(Pageable pageable) { … }
}

自己写业务接口时照抄这个模式:

@PreAuthorize("hasAuthority('ORDER_APPROVE')")
public OrderDTO approve(@PathVariable Long id) { … }

hasAuthority('X') 匹配精确权限,hasRole('ADMIN') 自动补 ROLE_ 前缀。更复杂的用 SpEL:hasAnyAuthority('A','B')authentication.name == #login(只能改自己)等。注解加在 Service 层还是 Resource 层?我们的实践:REST 层加粗授权(哪个角色能碰这组接口),Service 层加细授权(业务约束,如”只能操作本租户数据”)——这样被其他 Service 复用时安全边界仍在。

第二道防线的意义:即使前端守卫被绕过(直接 curl),没权限的请求在方法调用前就被拦下,返回 403。

第三道防线:前端路由守卫与权限判断

后端拦得住,但产品要的是”没权限的菜单别显示”。JHipster 前端(Angular)生成的守卫体系:

// 路由级:userAuthoritiesGuard,声明所需权限
export const adminRoutes: Routes = [
  {
    path: 'admin',
    canActivate: [userAuthoritiesGuard],
    data: { authorities: [Authority.ADMIN] },   // 不满足重定向 403 页或首页
    children: [ /* 用户管理、审计、配置… */ ],
  },
];

模板/组件里的条件渲染用生成的 AccountService:

@if (accountService.hasAnyAuthority('ADMIN')) {
  <a routerLink="/admin/user-management">用户管理</a>
}

必须强调:前端守卫只是体验层,藏起来的按钮不代表接口安全,真正的边界永远在后端。React 版(19)对应的是生成路由里的 authority 元数据 + useAccount 类 hook,思想完全一致。

实战:自定义权限的完整流程

需求:只有客服主管能”关闭订单”。从零加一个 ORDER_CLOSE 权限,四步走完前后端。

第一步:Liquibase 注入权限与角色绑定(第 7 篇的增量 changeset 规范):

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" ...>
  <changeSet id="20260824150000-1" author="charlie">
    <insert tableName="jhi_authority">
      <column name="name" value="ORDER_CLOSE"/>
    </insert>
    <!-- 挂给 ROLE_ADMIN -->
    <insert tableName="jhi_user_authority">
      <column name="user_id" value="1"/>
      <column name="authority_name" value="ORDER_CLOSE"/>
    </insert>
  </changeSet>
</databaseChangeLog>

实际项目通常引入第三个概念 jhi_role(或直接以 authority 分组)来管理批量绑定,思路一致:权限变更也是数据库变更,走 Liquibase,这样所有环境自动同步。

第二步:后端方法授权

public static final String ORDER_CLOSE = "ORDER_CLOSE";  // AuthoritiesConstants

@DeleteMapping("/api/orders/{id}")
@PreAuthorize("hasAuthority('" + AuthoritiesConstants.ORDER_CLOSE + "')")
public ResponseEntity<Void> closeOrder(@PathVariable Long id) { … }

第三步:前端声明与判断。Angular 侧把权限常量补进生成的 Authority 枚举,路由 data.authorities、模板 hasAnyAuthority('ORDER_CLOSE') 照生成模式加。

第四步:验证闭环

# 无 token:401
curl -i http://localhost:8080/api/orders/1 -X DELETE
# user token:403
# admin(已挂权限):204

一个 JWT 时代的已知影响(第 8 篇埋的伏笔):权限存在 token 的 auth claim 里,改了用户的权限不会立即生效,要等 token 过期重新登录。JHipster 默认有效期不长,可接受;不能接受就缩短有效期或在网关/过滤器层做实时查询。

小结

  • 模型:jhi_userjhi_user_authorityjhi_authority,authority 就是一串文本,ROLE_ 前缀约定区分角色与权限。
  • 三道防线:SecurityConfiguration 的 URL 级兜底规则、@PreAuthorize 方法级主力(REST 层粗、Service 层细)、前端路由守卫与 hasAnyAuthority 只管体验。
  • 自定义权限四步:Liquibase 插 authority → 方法注解 → 前端常量与守卫 → curl 验证 401/403/204。
  • 前端绕过不等于越权,后端才是边界;权限变更经 Liquibase 全环境同步,token 中的权限有生效延迟。

系列导航

← 集合源码 009:WeakHashMap源码解析 目录 网络协议 009:知识点串联:输入URL 到页面加载过程详解 →
← 返回文章列表