You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot gRPC项目集成JWT认证方案咨询及代码修正指导

gRPC Spring Boot集成JWT认证问题

我在Spring Boot项目中使用yidongnan/grpc-spring-boot-starter实现gRPC功能,现在想为gRPC接口集成JWT身份认证。编写了以下代码,但只有客户端不携带认证信息时请求才能正常通过,携带Token时请求失败,不知道该如何修改。

客户端配置

@Slf4j
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {

    @Value("${auth.token}")
    private String token;

    @Bean
    public StubTransformer call() {
        return (name, stub) -> {
            if ("local-grpc-server".equals(name)) {
                return stub.withCallCredentials(CallCredentialsHelper.bearerAuth(token)); // 注释掉这行请求就能通过
            }
            return stub;
        };
    }

}

服务端配置与实现

JWT工具类(修正版)

@Slf4j
@Component
public class JwtTokenProvider {

    private static final String AUTHORITIES_KEY = "roles";

    private final JwtProperties jwtProperties;

    @Getter
    private NimbusJwtDecoder jwtDecoder;

    private SecretKey secretKey;

    public JwtTokenProvider(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
        // 统一密钥处理:优先尝试Base64解码配置的密钥,失败则直接用明文
        byte[] keyBytes;
        try {
            keyBytes = Base64.getDecoder().decode(jwtProperties.getSignKey());
        } catch (IllegalArgumentException e) {
            keyBytes = jwtProperties.getSignKey().getBytes(StandardCharsets.UTF_8);
        }
        this.secretKey = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
        this.jwtDecoder = NimbusJwtDecoder.withSecretKey(this.secretKey).build();
    }

    public String createToken(Authentication authentication) {
        String username = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Claims claims = Jwts.claims().setSubject(username);
        if (!authorities.isEmpty()) {
            claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        }

        Date now = new Date();
        Date validity = new Date(now.getTime() + this.jwtProperties.getExpireTimeAsSec() * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(this.secretKey, SignatureAlgorithm.HS256)
                .compact();
    }

    public Authentication getAuthentication(String token) {
        Jwt jwt;
        try {
            jwt = jwtDecoder.decode(token);
        } catch (JwtException e) {
            throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
        }

        Object authoritiesClaim = jwt.getClaims().get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities = authoritiesClaim == null 
                ? AuthorityUtils.NO_AUTHORITIES
                : ((List<?>) authoritiesClaim).stream()
                        .map(auth -> new SimpleGrantedAuthority(auth.toString()))
                        .collect(Collectors.toList());

        return new JwtAuthenticationToken(jwt, authorities);
    }

    public boolean validateToken(Jwt token) {
        try {
            // 仅验证Token过期时间,避免默认验证器要求未设置的issuer/audience字段
            OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator());
            OAuth2TokenValidatorResult result = validator.validate(token);
            return !result.hasErrors();
        } catch (JwtException | IllegalArgumentException e) {
            throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
        }
    }

}

用户仓库

@Repository
public class UserRepository {

    private static final Map<String, UserDetails> allUsers = new HashMap<>();

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    protected void init() {
        allUsers.put("pkslow", new UserDetailsImpl("pkslow", passwordEncoder.encode("123456"), new SimpleGrantedAuthority("ROLE_ADMIN")));
        allUsers.put("user", new UserDetailsImpl("user", passwordEncoder.encode("123456"), new SimpleGrantedAuthority("ROLE_USER")));
    }

    public Optional<UserDetails> findByUsername(String username) {
        return Optional.ofNullable(allUsers.get(username));
    }
}

用户详情服务

@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository users;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found"));
    }
}

服务端安全配置(修正版)

@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true)
public class SecurityConfiguration {

    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider(
            final CustomUserDetailsService userDetailsService,
            final PasswordEncoder passwordEncoder) {

        final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }

    @Bean
    AuthenticationManager authenticationManager(final DaoAuthenticationProvider daoAuthenticationProvider) {
        final List<AuthenticationProvider> providers = new ArrayList<>();
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    GrpcAuthenticationReader authenticationReader(final JwtTokenProvider jwtTokenProvider) {
        BearerAuthenticationReader bearerReader = new BearerAuthenticationReader(
                accessToken -> {
                    Jwt jwt = jwtTokenProvider.getJwtDecoder().decode(accessToken);
                    jwtTokenProvider.validateToken(jwt);
                    return jwtTokenProvider.getAuthentication(accessToken);
                }
        );
        return new CompositeGrpcAuthenticationReader(Collections.singletonList(bearerReader));
    }

    // 关键:配置gRPC请求的安全过滤链,启用认证
    @Bean
    SecurityFilterChain grpcSecurityFilterChain(HttpSecurity http, JwtTokenProvider jwtTokenProvider) throws Exception {
        http
                .securityMatcher(GrpcWebSecurityMatcher.INSTANCE)
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtTokenProvider.getJwtDecoder())));
        return http.build();
    }

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

核心修改点说明

  1. 密钥处理修复:原代码签名与验证的密钥不一致,改为统一使用SecretKey处理,避免Base64编码/解码的重复错误。
  2. 权限解析修复:原代码将JWT中存储的权限List转为字符串再解析,导致权限格式错误,改为直接从List转换为SimpleGrantedAuthority。
  3. Token验证逻辑简化:移除默认验证器对issuer/audience的强制校验,仅保留过期时间验证,匹配生成Token时的字段设置。
  4. 新增gRPC安全过滤链:原代码未启用gRPC接口的认证逻辑,新增grpcSecurityFilterChain配置,确保携带Token的请求会触发认证流程。
  5. 客户端Token格式检查:确保${auth.token}配置的是纯JWT字符串,CallCredentialsHelper.bearerAuth()会自动添加Bearer 前缀。

内容的提问来源于stack exchange,提问作者tony

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 13:18:04