如何在不依赖第三方 JWT 角色声明的情况下安全地从后端获取并验证用户角色

大萱姑娘_6364

大萱姑娘_6364

2026-05-07

382人浏览

原创

本文介绍一种安全、可靠的方式:前端使用 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;
    }
}</grantedauthority>

⚠️ 注意:上述配置仅用于 后端资源保护(即 /api/** 接口的 @PreAuthorize 或 hasRole() 生效),但不解决前端角色展示问题——因为 Angular 仍需知道当前用户角色以渲染菜单或按钮。

FD Studio
FD Studio

一款AI视频创作工具,主要用于一站式AI辅助影视创意工作台,适合需要提升相关任务效率的用户。

下载

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>> 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);
}</string></string></claims></string></map>

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 传递,才能真正实现端到端可信授权。

相关专题

更多
python是前端还是后端
python是前端还是后端

Python属于前端也属于后端,其灵活性和丰富的生态系统使得开发人员能够在不同的领域中灵活运用。本专题为大家提供python相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.11

2703

5

前端和后端的区别
前端和后端的区别

前端关注的是用户界面的设计和交互,而后端则注重数据处理和逻辑控制。想了解更多前端后端的相关内容,可以阅读本专题下面的文章。

2024.03.19

6850

13

后端的主要工作内容介绍
后端的主要工作内容介绍

后端是应用程序的服务端部分,负责核心任务,如数据库交互、业务逻辑处理和响应客户端请求。想了解更多后端的相关内容,可以阅读本专题下面的文章。

2024.03.19

6306

10

js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

2023.06.20

5326

5

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

2023.07.04

1309

3

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

2023.07.04

5244

6

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

2023.09.01

1000

4

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

2023.09.04

1936

5

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

2023.09.04

3705

4

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
LLVM官方文档
LLVM官方文档

共0课时 | 0人学习