java.lang.非法参数异常:如果对指定的JWT进行了数字签名,则必须指定签名密钥

50pmv0ei  于 2022-12-17  发布在  Java
关注(0)|答案(3)|浏览(131)

我希望在我的应用程序中实现JWT,为此我正在进行一些研发,参考了以下内容:https://stormpath.com/blog/jwt-java-create-verify。当我试图通过提取声明集来实现verifyToken()时,我成功地实现了generateToken()方法。我不明白apiKey.getSecret()是从哪里来的。您能指导我吗?
下面的代码可供参考:

public class JJWTDemo {

    private static final String secret = "MySecrete";

    private static String generateToken(){
        String id = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();

        return token;
    }

    private static void verifyToken(String token){
        Claims claims = Jwts.parser().
                setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
                .parseClaimsJws(token).getBody();
        System.out.println("----------------------------");
        System.out.println("ID: " + claims.getId());
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("Issuer: " + claims.getIssuer());
        System.out.println("Expiration: " + claims.getExpiration());
    }

    public static void main(String[] args) {
        System.out.println(generateToken());
        String token = generateToken();
        verifyToken(token);
    }
}

我看到下面的错误即将到来:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4N2E5NmYwNTcyN2M0ZDY0YjZmODlhNDAyOTQ2OTZiNyIsImlhdCI6MTQ4NDQ4NjYyNiwibmJmIjoxNDg0NDg2NjI2LCJleHAiOjE0ODQ0ODY2NTZ9.ycS7nLWnPpe28DM7CcQYBswOmMUhBd3wQwfZ9C-yQYs
Exception in thread "main" java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed.
    at io.jsonwebtoken.lang.Assert.notNull(Assert.java:85)
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:331)
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:481)
    at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541)
    at io.jsonwebtoken.jjwtfun.service.JJWTDemo.verifyToken(JJWTDemo.java:31)
    at io.jsonwebtoken.jjwtfun.service.JJWTDemo.main(JJWTDemo.java:41)

Maven依赖项:

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
<jjwt.version>0.7.0</jjwt.version>
xpszyzbs

xpszyzbs1#

blog文章中的apiKey.getSecret()引用了安全的、随机生成的、Base64编码的密钥分配给Stormpath为每个客户提供的API密钥的(类似密码)。Stormpath客户使用此API密钥对进入Stormpath REST API的每个请求进行身份验证。因为每个Stormpath客户都有一个API密钥(并且您的应用程序可以访问该密钥),API密钥秘密是签名和验证特定于您的应用程序的JWT的理想“默认值”。
如果没有Stormpath API Key,任何足够强的安全随机字节数组都可以用于签名和验证JWT。
在上面的示例中,以下内容显示为测试密钥:

private static final String secret = "MySecrete";

这不是有效的(符合JWT的)密钥,不能用于JWT HMAC算法。
JWT RFC * 要求 * 您必须使用等于或大于散列输出长度的字节数组密钥长度。
这意味着如果使用HS 256、HS 384或HS 512,密钥字节数组必须分别为256位(32字节)、384位(48字节)或512位(64字节)。我将在another StackOverflow answer中对此进行更详细的介绍-请参阅那里的数据,以及可以生成符合规范的安全密钥的MacProvider示例。
在此基础上,下面是代码示例,重写为a)生成一个有效的密钥,b)将该密钥引用为Base64编码的字符串:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.MacProvider;

import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

public class JJWTDemo {

    private static final Key secret = MacProvider.generateKey(SignatureAlgorithm.HS256);
    private static final byte[] secretBytes = secret.getEncoded();
    private static final String base64SecretBytes = Base64.getEncoder().encodeToString(secretBytes);

    private static String generateToken() {
        String id = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000 * 30)); // 30 seconds

        String token = Jwts.builder()
            .setId(id)
            .setIssuedAt(now)
            .setNotBefore(now)
            .setExpiration(exp)
            .signWith(SignatureAlgorithm.HS256, base64SecretBytes)
            .compact();
        
        return token;
    }

    private static void verifyToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(base64SecretBytes)
            .parseClaimsJws(token).getBody();
        System.out.println("----------------------------");
        System.out.println("ID: " + claims.getId());
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("Issuer: " + claims.getIssuer());
        System.out.println("Expiration: " + claims.getExpiration());
    }

    public static void main(String[] args) {
        System.out.println(generateToken());
        String token = generateToken();
        verifyToken(token);
    }
}

注意,Base64编码的字节数组是 not encrypted(文本编码!=加密),所以确保如果你对你的密钥字节进行Base64编码,你仍然保持Base64字符串的安全/隐藏。
最后,上面的最终静态常量(命名为secretsecretBytesbase64SecretBytes)仅用于这个简单的测试演示--不应将密钥硬编码到源代码中,更不用说将其设为静态常量,因为它们很容易被反编译。

k4ymrczo

k4ymrczo2#

我100%同意Les Hazlewood。但是我们应该总是发送SubjectIssuerAudience来识别当前登录用户的更多细节。代码可以修改如下:

import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.MacProvider;

public class TokenUtil {
    private static final Key secret = MacProvider.generateKey(SignatureAlgorithm.HS256);
    private static final byte[] secretBytes = secret.getEncoded();
    private static final String base64SecretBytes = Base64.getEncoder().encodeToString(secretBytes);

    private static String generateToken(String subject, String issuer, String audience) {
        String id = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000 * 30)); // 30 seconds

        String token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .setSubject(subject)
                .setIssuer(issuer)
                .setAudience(audience)
                .signWith(SignatureAlgorithm.HS256, base64SecretBytes)
                .compact();

        return token;
    }

    private static void verifyToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(base64SecretBytes)
                .parseClaimsJws(token).getBody();

        System.out.println("----------------------------");
        System.out.println("ID: " + claims.getId());
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("Issuer: " + claims.getIssuer());
        System.out.println("Expiration : " + claims.getExpiration());
        System.out.println("Not Before : "+claims.getNotBefore());
        System.out.println("Audience :: "+claims.getAudience());
    }

    public static void main(String[] args) {
        String token = generateToken("MySubject", "AH", "MyAudience");
        System.out.println("TOKEN :: "+token);
        verifyToken(token);
    }
}
mwkjh3gx

mwkjh3gx3#

要生成令牌:

AppUser appUserPrincipal = (AppUser) authentication.getPrincipal();

    return Jwts.builder()
            .setSubject((appUserPrincipal.getUsername()))
            .setIssuedAt(new Date())
            .signWith(getKey(), SignatureAlgorithm.HS512)
            .compact();
    }

    public Key getKey() {
      byte[] keyByte = Decoders.BASE64.decode(jwtSecret);
      return Keys.hmacShaKeyFor(keyByte);
    }

然后使用以下代码从令牌获取userName(主题):

public String getUserNameFromJwtToken(String token) {
          return Jwts.parserBuilder().setSigningKey(getKey()).build()
            .parseClaimsJws(token).getBody().getSubject();
    }

相关问题