自定义授权令牌生成错误致Spring Security认证提供者缺失,求修复
问题描述
我需要生成符合如下格式的自定义授权令牌:
universal-auth-key:FE-DP-0-570bb6ae-0-302d02150083da0fdf85b657d01f8530a54...dc349f15bc5aa9491 id: universal-auth-key keyType: FE keyAlias: DP expiryTimeInHex: 0 issueTimeInHex: 530fbbro keySequenceNumber: 0 signature: i3p8v71wbwiyrwm7q3r459hzcyqj4eh9vn7g8q1v0ppvtytuupwe6hh
我编写了如下代码尝试实现:
public KeyPair createKeyPair() { try { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA", "BC"); keyPairGenerator.initialize(2048, new SecureRandom()); KeyPair keyPair = keyPairGenerator.generateKeyPair(); return keyPair; } catch (NoSuchAlgorithmException | NoSuchProviderException e) { throw new RuntimeException(e); } } .... public X509Certificate createX509Certificate(KeyPair keyPair) { try { // Define certificate details X500Name issuer = new X500Name("CN=Test"); X500Name subject = new X500Name("CN=Test"); BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30); Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10)); // Generate a self-signed certificate X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuer, serial, notBefore, notAfter, subject, keyPair.getPublic()); // Add Basic Constraints to mark this as a non-CA certificate certBuilder.addExtension(org.bouncycastle.asn1.x509.Extension.basicConstraints, true, new BasicConstraints(false)); // Sign the certificate ContentSigner signer = new JcaContentSignerBuilder("SHA1withDSA").setProvider("BC").build(keyPair.getPrivate()); // Convert to X509Certificate X509Certificate certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certBuilder.build(signer)); return certificate; } catch (CertIOException | OperatorCreationException | CertificateException e) { e.printStackTrace(); } return null; } ..... String x509CertificateAsString = Base64.getEncoder().encodeToString(x509Certificate.getEncoded()); ...... String result = createBasicTokenPassword(keyPair.getPrivate(), createApiKeyParams.getUsername(), createApiKeyParams.getAlias()); ..... private String createBasicTokenPassword(PrivateKey privateKey, String username, String alias) { try { String data = String.format("FE-%s-%s-%s-%s", alias, // keyAlias generateTokenExpireTimeInHex(), // expiryTimeInHex generateTokenIssueTimeInHex(), // issueTimeInHex generateTokenSequenceNumber() // keySequenceNumber ); String privateKeySignature = certificateService.getPrivateKeySignature(privateKey, username + data); String format = String.format("%s-%s", data, privateKeySignature);// signature return BaseEncoding.base64().encode(format.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException(e); } }
最终我返回了Base64.getEncoder().encodeToString(result.toString().getBytes());的结果,但使用该令牌时,Spring Security抛出如下错误:
2024-09-23T18:40:48.654+03:00 DEBUG 23960 --- [microservice] [nio-8180-exec-1] o.s.s.w.a.www.BasicAuthenticationFilter : Failed to process authentication request org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:234) ~[spring-security-core-6.2.5.jar:6.2.5] at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:187) ~[spring-security-web-6.2.5.jar:6.2.5] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.10.jar:6.1.10] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.2.5.jar:6.2.5] at org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter.doFilterInternal(DefaultLogoutPageGeneratingFilter.java:58) ~[spring-security-web-6.2.5.jar:6.2.5] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.10.jar:6.1.10] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.2.5.jar:6.2.5] at
看起来令牌生成方式有误,请问如何修复我的代码?
修复方案
1. 修正令牌格式,匹配目标要求
当前生成的令牌和目标格式存在多处不符:
- 缺少目标令牌开头的
universal-auth-key:前缀 - 重复进行Base64编码(
createBasicTokenPassword已返回Base64结果,后续又再次编码) - 核心字段拼接顺序和目标结构不一致
调整后的令牌生成代码:
private String createBasicTokenPassword(PrivateKey privateKey, String username, String alias) { try { String keyType = "FE"; String expiryHex = generateTokenExpireTimeInHex(); String issueHex = generateTokenIssueTimeInHex(); String sequence = generateTokenSequenceNumber(); // 按目标格式拼接核心字段 String tokenCore = String.format("%s-%s-%s-%s-%s", keyType, alias, expiryHex, issueHex, sequence); // 生成签名(确保签名是目标要求的明文字符串格式,而非二进制Base64) String signature = certificateService.getPrivateKeySignature(privateKey, username + tokenCore); // 组合最终令牌,添加前缀 String finalToken = String.format("universal-auth-key:%s-%s", tokenCore, signature); // 无需额外Base64编码(目标示例为明文结构) return finalToken; } catch (Exception e) { throw new RuntimeException(e); } }
同时删除后续重复的Base64编码步骤:
// 原错误代码:Base64.getEncoder().encodeToString(result.toString().getBytes()); // 直接返回createBasicTokenPassword的结果 return result;
2. 配置Spring Security自定义认证逻辑
错误提示说明Spring Security默认的Basic认证过滤器无法识别你的自定义令牌,需要添加自定义认证逻辑:
示例Spring Security 6+配置:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) // 添加自定义令牌过滤器,替换默认Basic认证逻辑 .addFilterBefore(customTokenAuthenticationFilter(), BasicAuthenticationFilter.class); return http.build(); } @Bean public CustomTokenAuthenticationFilter customTokenAuthenticationFilter() { return new CustomTokenAuthenticationFilter(authenticationManager()); } @Bean public AuthenticationManager authenticationManager() { return new ProviderManager(List.of(new CustomTokenAuthenticationProvider())); } } // 自定义认证提供者,处理令牌验证 class CustomTokenAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { CustomTokenAuthenticationToken token = (CustomTokenAuthenticationToken) authentication; // 实现你的令牌验证逻辑:解析令牌结构、校验签名、验证有效期等 // 验证通过后返回已认证的令牌 return new UsernamePasswordAuthenticationToken("authenticated-user", null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); } @Override public boolean supports(Class<?> authentication) { return CustomTokenAuthenticationToken.class.isAssignableFrom(authentication); } } // 自定义过滤器,提取请求中的令牌 class CustomTokenAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; public CustomTokenAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); // 假设令牌通过Authorization头传递,前缀为CustomToken if (authHeader != null && authHeader.startsWith("CustomToken ")) { String token = authHeader.substring("CustomToken ".length()); CustomTokenAuthenticationToken authRequest = new CustomTokenAuthenticationToken(token); Authentication authResult = authenticationManager.authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication(authResult); } filterChain.doFilter(request, response); } } // 自定义认证令牌类 class CustomTokenAuthenticationToken extends AbstractAuthenticationToken { private final String token; public CustomTokenAuthenticationToken(String token) { super(null); this.token = token; setAuthenticated(false); } @Override public Object getCredentials() { return token; } @Override public Object getPrincipal() { return null; } }
3. 修正签名格式和算法
- 确保
getPrivateKeySignature方法返回的是目标要求的明文签名字符串(比如十六进制格式),而非DSA默认的ASN.1二进制编码 - 替换不安全的SHA1算法为SHA256,修改证书生成时的签名算法:
ContentSigner signer = new JcaContentSignerBuilder("SHA256withDSA").setProvider("BC").build(keyPair.getPrivate());
内容的提问来源于stack exchange,提问作者Peter Penzov
相关产品推荐
相关产品推荐

