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(); } }
核心修改点说明
- 密钥处理修复:原代码签名与验证的密钥不一致,改为统一使用
SecretKey处理,避免Base64编码/解码的重复错误。 - 权限解析修复:原代码将JWT中存储的权限List转为字符串再解析,导致权限格式错误,改为直接从List转换为
SimpleGrantedAuthority。 - Token验证逻辑简化:移除默认验证器对issuer/audience的强制校验,仅保留过期时间验证,匹配生成Token时的字段设置。
- 新增gRPC安全过滤链:原代码未启用gRPC接口的认证逻辑,新增
grpcSecurityFilterChain配置,确保携带Token的请求会触发认证流程。 - 客户端Token格式检查:确保
${auth.token}配置的是纯JWT字符串,CallCredentialsHelper.bearerAuth()会自动添加Bearer前缀。
内容的提问来源于stack exchange,提问作者tony
相关产品推荐
相关产品推荐

