本文介绍一种安全、可靠的方式:前端使用 google oauth2 登录后,后端校验令牌有效性并查询数据库获取用户角色,再签发自有 jwt(含角色信息),前端仅信任该自有令牌——所有权限控制均基于服务端签名验证,杜绝客户端篡改风险。
本文介绍一种安全、可靠的方式:前端使用 google oauth2 登录后,后端校验令牌有效性并查询数据库获取用户角色,再签发自有 jwt(含角色信息),前端仅信任该自有令牌——所有权限控制均基于服务端签名验证,杜绝客户端篡改风险。
在现代前后端分离架构中,直接依赖第三方身份提供商(如 Google)颁发的 Access Token 进行前端权限判断存在根本性安全隐患:Google 的 JWT 不包含 roles、authorities 等业务级声明,且其签名仅保证由 Google 签发,不承诺角色数据的真实性与完整性。若前端直接解析并信任后端返回的“角色数组”,攻击者可通过拦截/篡改响应(如 MitM 或浏览器调试工具)伪造高权限角色,绕过 UI 层权限控制。
✅ 正确做法是:将第三方登录视为“身份认证”阶段,而非“授权”阶段。真正的角色授权必须由你的业务系统独立完成,并通过服务端签发的可信凭证传递给前端。
1. 后端流程重构(Spring Boot 示例)
首先,禁用对 Google Token 的直接角色映射,改为自定义 JwtAuthenticationConverter,仅提取用户标识(如 sub 或 email),再通过 UserDetailsService 查询数据库加载角色:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.jwtAuthenticationConverter(customJwtAuthConverter()) // ← 关键:自定义转换器
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter customJwtAuthConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix(""); // 避免默认添加 "ROLE_"
authoritiesConverter.setAuthoritiesClaimName("dummy"); // 忽略 Google 原始 roles
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(token -> {
String email = token.getClaimAsString("email");
// ✅ 查询数据库获取真实角色(如 ADMIN, USER)
Collection<GrantedAuthority> roles = userService.loadUserRolesByEmail(email);
return roles;
});
return converter;
}
}⚠️ 注意:上述配置仅用于 后端资源保护(即 /api/** 接口的 @PreAuthorize 或 hasRole() 生效),但不解决前端角色展示问题——因为 Angular 仍需知道当前用户角色以渲染菜单或按钮。
2. 前端安全角色同步方案
绝对禁止让前端直接信任任意后端 API 返回的 ["ADMIN", "EDITOR"] 数组。正确路径是:
- 用户首次登录 Google 后,Angular 将 Google Access Token 发送给你的 自有认证接口(如 /auth/login-with-google);
- 后端校验 Google Token 有效性(JWK 验证),提取 email,查询 DB 获取角色,签发你自己的 JWT(含 roles、exp、iss 等标准字段,并使用私钥签名);
- 前端存储该自有 JWT(推荐 HttpOnly Cookie 或内存中保管),后续所有请求携带它;
- Angular 的 AuthGuard 和权限指令(如 *appHasRole="ADMIN")只解析并信任该自有 JWT 的 payload,绝不依赖任何非签名响应体。
示例自有 JWT 签发逻辑(Spring Security + jjwt-api):
// AuthController.java
@PostMapping("/auth/login-with-google")
public ResponseEntity<Map<String, String>> loginWithGoogle(
@RequestBody Map<String, String> request) {
String googleToken = request.get("token");
// 1. 使用 Nimbus JOSE JWT 验证 Google Token 签名 & exp
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(rsaPublicKey) // Google 公钥
.parseClaimsJws(googleToken);
String email = claimsJws.getBody().get("email", String.class);
List<String> roles = roleService.findRolesByEmail(email);
// 2. 签发自有 JWT(使用 YOUR 私钥!)
String yourJwt = Jwts.builder()
.setSubject(email)
.claim("roles", roles)
.setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1h
.signWith(yourPrivateKey, SignatureAlgorithm.RS256)
.compact();
Map<String, String> response = new HashMap<>();
response.put("token", yourJwt); // ← Angular 只信任这个 token!
return ResponseEntity.ok(response);
}3. 前端 Angular 实践要点
- 使用 @auth0/angular-jwt 解析自有 JWT,不要解析 Google Token;
- 角色检查必须基于 jwtHelper.decodeToken(token).roles,且每次路由守卫(CanActivate)都应重新解析;
- 敏感操作(如删除资源)的按钮点击事件,必须再次调用后端鉴权接口(如 POST /api/resources/{id}/delete),而非仅依赖前端角色缓存;
- 自有 JWT 应设置合理过期时间(如 1 小时),配合 Refresh Token 机制延长会话。
总结
| 错误做法 | 正确做法 |
|---|---|
| 前端解析 Google Token 获取角色 | Google Token 仅用于身份认证,角色由后端查库后签发自有 JWT |
| 前端信任 /api/user/roles 接口返回的角色数组 | 所有角色信息必须来自已签名、可验证的自有 JWT payload |
| 在前端硬编码角色字符串做权限判断 | 权限逻辑应与 JWT 解析解耦,结合后端细粒度鉴权(如 @PreAuthorize("hasPermission(#id, 'DELETE')")) |
最终,安全性不取决于“谁签发了令牌”,而在于“谁控制了角色数据的生成与签名密钥”。将角色权威完全收归自有后端,并通过强签名 JWT 传递,才能真正实现端到端可信授权。

















