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

自定义授权令牌生成错误致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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 23:34:50