安全分两讲:本篇讲认证(你是谁),下一篇讲授权(你能干什么)。认证是 JHipster 生成质量最高的部分之一——选型答完,登录页、token 管理、过滤器链、前端守卫全部就位。但”生成的好”不等于”不用懂”,出安全问题时没有基础知识会很被动。
三种认证方式选型
JHipster 生成时的 authenticationType 有三个主选项:
| 维度 | JWT | OAuth 2.0 / OIDC | Session |
|---|---|---|---|
| 原理 | 服务端签名自包含 token | 委托外部 IdP(Keycloak 等)签发 | 服务端保存会话,Cookie 关联 |
| 外部依赖 | 无(dev 内存用户即用) | 必须:Keycloak/Okta/企业 IdP | 无 |
| 服务端状态 | 无状态,水平扩展友好 | 无状态(校验走签名或内省) | 有状态(或交给 Hazelcast 共享) |
| 注销 | token 到期前仍有效(需黑名单兜底) | IdP 管理,相对完整 | 立即失效 |
| 多系统 SSO | 需自建或改造 | 天然支持 | 不适合 |
| 密码策略/MFA | 自己实现 | IdP 现成能力 | 自己实现 |
我们的选型经验:
- 内网业务系统、独立部署:JWT,零依赖最省心;
- 公司有统一身份、或多个系统要单点登录、或要接 MFA:OIDC(Keycloak 自建或对接企业 IdP),JHipster 生成 Spring Security 的 OAuth2 client 配置,前端走 OIDC 授权码 + PKCE 流程;
- 传统管理后台、用户量小、注销要求严格:Session 也可以,但现代项目里我们基本不选了。
微服务架构下另有 JWT 网关统一校验与服务间透传的话题(第 5 篇),本篇以单体为主线。
JWT 流程原理
JHipster 生成的 JWT 认证时序:
sequenceDiagram
participant B as 浏览器(Angular)
participant F as JwtAuthenticationFilter
participant C as AuthenticateController
participant S as Spring Security
participant T as TokenProvider
B->>C: POST /api/authentication {username, password}
C->>S: AuthenticationManager.authenticate(token)
S-->>C: Authentication(含 authorities)
C->>T: createToken(auth, rememberMe)
T-->>C: JWT(Header.Payload.Signature,HS512 签名)
C-->>B: 200 { id_token: "eyJhbGci..." }
Note over B: 存 localStorage,后续请求带<br/>Authorization: Bearer <token>
B->>F: GET /api/customers(带 Bearer)
F->>T: getAuthentication(token)(验签 + 解析权限)
T-->>F: Authentication(UsernamePasswordAuthenticationToken)
F->>S: 放入 SecurityContext
S-->>B: 200 业务数据
三个要点:
- token 的可信度来自签名:payload 里带 subject(login)与 auth(权限列表),HS512 密钥配置在
application.yml的jhipster.security.authentication.jwt.secret(生产必须用环境变量覆盖且足够长);密钥不泄,token 不可伪造。 - 无状态:服务端不存会话,校验就是本地验签,横向扩容不需要粘滞会话(配合 Hazelcast 的 HTTP session 复制是另一件事,第 10 篇)。
- 无状态的代价是注销不即时:JWT 到期前一直有效。JHipster 默认有效期较短(rememberMe 勾选时更长),高敏感系统自己加黑名单缓存。
生成代码解读
JWT 认证的核心生成物在 security/ 与 web/rest/ 下:
TokenProvider——签发与解析:
@Component
public class TokenProvider {
private static final String AUTHORITIES_KEY = "auth";
@Value("${jhipster.security.authentication.jwt.secret}")
private String secretKey;
@Value("${jhipster.security.authentication.jwt.token-validity-in-seconds}")
private long tokenValidity;
public String createToken(Authentication authentication, boolean rememberMe) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = new Date().getTime();
Date validity = new Date(now + (rememberMe ? tokenValidityRememberMe : tokenValidity) * 1000);
return Jwts.builder()
.subject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.expiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token).getPayload();
var authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(claims.get(AUTHORITIES_KEY));
var principal = new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
}
(节选,包名随项目。)值得读的两点:权限以逗号拼接塞进 auth claim;解析时从 claim 还原 authorities——token 里带权限是 JHipster JWT 的设计选择,权限变更要等 token 刷新才生效,第 9 篇自定义权限时会再遇到这个影响。
JwtAuthenticationFilter——每请求校验:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String jwt = resolveToken(request); // 解析 Authorization: Bearer xxx
if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
逻辑朴素:有 token、验签通过,就构造 Authentication 放进 SecurityContext,后续的授权判断(第 9 篇的 @PreAuthorize)都从这取身份。它被 SecurityConfiguration 挂进过滤器链(下一篇详解该配置类)。
前端配套生成的是 AuthServerProvider/登录组件——token 存 localStorage,HTTP 拦截器统一注入 Authorization 头,401 时统一跳登录。
dev 的内存用户
JWT + dev profile 下无需任何数据库操作即可登录(第 2 篇提过):Spring Security 的 UserDetailsService 走内存实现,内置 admin/admin 与 user/user。这些账号与权限其实由 Liquibase 的种子数据写入 jhi_user/jhi_authority 表,dev 用内存映射只是为了启动即用。生产环境第一次部署务必改掉默认密码(用户管理界面即可,第 9 篇讲用户-角色-权限表结构)。
OAuth2 / OIDC 与 Keycloak
选 OAuth2/OIDC 后,生成的后端用 Spring Security OAuth2 client,前端走授权码 + PKCE。本地开发的标准姿势是 Keycloak docker-compose(JHipster 生成在 src/main/docker/realm-config/ 带好 realm 配置):
docker compose -f src/main/docker/keycloak.yml up -d
# Keycloak 起在 :9080,realm 已含测试用户 admin/admin
./mvnw # 后端对接 :9080 的 issuer
npm start
浏览器访问应用会被重定向到 Keycloak 登录页,认证后带 code 回跳换 token。相比 JWT 模式,认证逻辑外移给了 IdP:密码策略、MFA、会话管理、多应用 SSO 都在 Keycloak 里配置。企业落地时的常见形态:测试环境用生成的 realm,生产对接 AD/企业 IdP,只改 application-prod.yml 里 issuer 与 client 凭据。
顺带一提 Spring Boot 4 带来的变化:Security 相关配置有一轮迁移,例如 WebSocket 安全不再用旧的消息级配置,改走 EnableWebSocketSecurity 体系——升级过来时 SockJS/stomp 端点的 CSRF 配置要按新写法检查一遍。
小结
- 选型:独立系统 JWT 零依赖;要 SSO/MFA/统一身份上 OIDC + Keycloak;Session 现代项目少选。
- JWT 流程:登录端点签发(HS512、payload 含权限)→ 前端存 localStorage 带 Bearer → 过滤器每请求验签还原 SecurityContext。
- 读代码抓三个文件:TokenProvider(签发/解析)、JwtAuthenticationFilter(请求校验)、SecurityConfiguration(链组装,下篇展开)。
- OIDC 本地一把梭:生成的 keycloak.yml + realm 配置;生产改 issuer 即接企业 IdP;默认密码上线必改。
系列导航
- 上一篇:数据库迁移——Liquibase 不是可选项
- 下一篇:安全体系(下)——授权
- 延伸阅读:技术栈全景图 · 环境搭建与第一个应用