JWT和RSA256签名

公众号:飞翔的代码

HTTP通信本身是无状态的,每次请求对于服务端来说都不知道是哪个用户的请求,因此才发明了cookie和session,用户登录后服务端保存session,并将session id作为cookie的一部分返回给用户,用户下次请求携带cookie,服务端根据cookie中的session id得到用户登录信息,这样服务端才能知道是哪个用户的请求

这是早期的实现方案,现在大部分都是分布式的系统,系统之间同步session是比较麻烦的,所以又有人发明了token,用户登录后服务端生成一个token字符串,将token返回给用户,用户每次请求携带token,在token中可以写入用户的基本信息,如用户名,服务端解析token得到用户名,这个过程可以使用AES或RSA加密技术来生成token

由于生成token后是直接返回给用户,服务端无法再对token进行限制,如果用户将token泄漏,那么别人拿着这个token就可以直接访问该用户的数据了,所以生成token时应该设置token的有效期,过期后不可使用。另一种方案是将token写入redis,这样可以在redis中删除某个token来起到让用户重新登录的功能。最终还是采用OAuth2.0方案更靠谱。

本文介绍通用的token方案JWT和JWK以及RSA256签名

JWT

全名Json Web Token
就是上面所说的token,由以下三部分组成:

  1. header 声明JWT的签名算法
  2. payload token中携带的明文数据
  3. signture 签名,一个JWT是否有效就看签名是否合法,防止伪造JWT

这三个部分各自base64后用点号拼接起来,第1和2部分base64解码后是明文的,因此千万不要在payload中写入重要的数据,通常用户ID也不要写入payload中,防止别人根据ID猜测用户量

JWK

全名Json Web Key
这是JWT的密钥信息,如果采用RSA加密方式签名,需要提供一个Public Key给校验方,校验方根据Public Key来验证JWT中的签名是否有效,如果直接将RSA的Public Key发给校验方也可以

HMAC256签名方式

这种签名方式不推荐,没有RSA256方式安全,以下列举其使用方法

pom.xml

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.2</version>
</dependency>
复制代码

工具类

public class JwtUtils {
    private static final String PREFIX = "密钥的前缀";
    private static final String SUFFIX = "密钥的后缀";

    /**
     * 产生token,永不过期
     *
     * @param userId 用户ID
     * @param ip     IP
     * @param secret 密钥
     * @return
     */
    public static String create(String userId, String ip, String secret) {
        return create(userId, ip, secret, null);
    }

    public static String create(String userId, String ip, String secret, String other) {
        return create(userId, ip, secret, other, null);
    }

    /**
     * 产生token
     *
     * @param userId 用户ID
     * @param ip     IP
     * @param secret 密钥
     * @param expire 过期时间,单位:毫秒
     * @return
     */
    public static String create(String userId, String ip, String secret, String other, Long expire) {
        JWTCreator.Builder builder = JWT.create().withAudience(userId, ip, String.valueOf(System.currentTimeMillis()), other);
        if (expire != null && expire > 0) {
            builder.withExpiresAt(new Date(System.currentTimeMillis() + expire));
        }
        return builder.sign(Algorithm.HMAC256(PREFIX + secret + SUFFIX));
    }

    /**
     * 获取token中的数据
     *
     * @param token token
     * @return
     * @throws TokenException
     */
    public static List<String> getAudienceList(String token) throws TokenException {
        try {
            return JWT.decode(token).getAudience();
        } catch (JWTDecodeException e) {
            throw new TokenException(e.getMessage());
        }
    }

    /**
     * 验证token是否有效
     *
     * @param token  token
     * @param secret 密钥
     * @return
     * @throws TokenException
     */
    public static boolean verify(String token, String secret) throws TokenException {
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(PREFIX + secret + SUFFIX)).build();
            jwtVerifier.verify(token);
            return true;
        } catch (JWTVerificationException e) {
            throw new TokenException(e.getMessage());
        }
    }
}
复制代码

用法

long expire = expireTime == null ? 7 * 24 * 3600 * 1000L : expireTime;
String token = JwtUtils.create(loginUser.getUsername(), HttpUtils.getIp(request), loginUser.getPassword(), other, expire);

// 验证token
if (JwtUtils.verify(token, loginUser.getPassword())) {
    // 得到payload中的数据
    List<String> data = JwtUtils.getAudienceList(token);
}
复制代码

将用户名、ip、当前时间和other写入payload,使用用户密码(加密后的密码)进行签名,这样的好处是每个用户的签名密钥都不相同

RSA256签名方式

pom.xml

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.0.1</version>
</dependency>
复制代码

Rsa工具类

主要是生成公私密钥和密钥转换(如字符串转密钥),一般长度至少要1024,推荐2048,对于安全要求更高的系统,可使用4096

@Slf4j
public class RsaUtils {

    private static final int KEY_SIZE = 2048;

    /**
     * 生成密钥对
     *
     * @return
     */
    public static KeyPair create() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(KEY_SIZE);
            return keyPairGenerator.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * 从字符串得到公钥
     *
     * @param key
     * @return
     */
    public static PublicKey getPublicKey(String key) throws InvalidKeySpecException {
        try {
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(initKey(key));
            KeyFactory factory = KeyFactory.getInstance("RSA");
            return factory.generatePublic(keySpec);
        } catch (InvalidKeySpecException e) {
            log.debug("公钥错误", e);
            throw e;
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * 从字符串得到私钥
     *
     * @param key
     * @return
     * @throws InvalidKeySpecException
     */
    public static PrivateKey getPrivateKey(String key) throws InvalidKeySpecException {
        try {
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(initKey(key));
            KeyFactory factory = KeyFactory.getInstance("RSA");
            return factory.generatePrivate(keySpec);
        } catch (InvalidKeySpecException e) {
            log.debug("私钥错误", e);
            throw e;
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * 公钥转字符串
     *
     * @param key
     * @return
     */
    public static String toPublicKeyString(PublicKey key) {
        return Base64.encodeBase64String(key.getEncoded());
    }

    /**
     * 私钥转字符串
     *
     * @param key
     * @return
     */
    public static String toPrivateKeyString(PrivateKey key) {
        return Base64.encodeBase64String(key.getEncoded());
    }

    /**
     * 使用私钥加密
     *
     * @param string
     * @param key
     * @return
     * @throws Exception
     */
    public static String encryptByPrivateKey(String string, String key) throws Exception {
        PrivateKey privateKey = getPrivateKey(key);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);

        // 由于长度限制,需要分开加密
        byte[] data = string.getBytes();
        int blockLength = KEY_SIZE / 8 - 11;
        int offset = 0;
        int i = 0;
        int length = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] cache;
        while (length - offset > 0) {
            if (length - offset > blockLength) {
                cache = cipher.doFinal(data, offset, blockLength);
            } else {
                cache = cipher.doFinal(data, offset, length - offset);
            }
            out.write(cache, 0, cache.length);
            i++;
            offset = i * blockLength;
        }

        byte[] bytes = out.toByteArray();
        out.close();

        return Base64.encodeBase64String(bytes);
    }

    /**
     * 使用公钥加密
     *
     * @param string
     * @param key
     * @return
     * @throws Exception
     */
    public static String encryptByPublicKey(String string, String key) throws Exception {
        PublicKey publicKey = getPublicKey(key);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        // 由于长度限制,需要分开加密
        byte[] data = string.getBytes();
        int blockLength = KEY_SIZE / 8 - 11;
        int offset = 0;
        int i = 0;
        int length = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] cache;
        while (length - offset > 0) {
            if (length - offset > blockLength) {
                cache = cipher.doFinal(data, offset, blockLength);
            } else {
                cache = cipher.doFinal(data, offset, length - offset);
            }
            out.write(cache, 0, cache.length);
            i++;
            offset = i * blockLength;
        }

        byte[] bytes = out.toByteArray();
        out.close();

        return Base64.encodeBase64String(bytes);
    }

    /**
     * 使用私钥解密
     *
     * @param string
     * @param key
     * @return
     * @throws Exception
     */
    public static String decryptByPrivateKey(String string, String key) throws Exception {
        PrivateKey privateKey = getPrivateKey(key);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        // 由于长度限制,需要分开加密
        byte[] data = Base64.decodeBase64(string);
        int length = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offset = 0;
        byte[] cache;
        int i = 0;
        int blockLength = KEY_SIZE / 8;
        while (length - offset > 0) {
            if (length - offset > blockLength) {
                cache = cipher.doFinal(data, offset, blockLength);
            } else {
                cache = cipher.doFinal(data, offset, length - offset);
            }
            out.write(cache, 0, cache.length);
            i++;
            offset = i * blockLength;
        }

        byte[] bytes = out.toByteArray();
        out.close();

        return new String(bytes, "utf-8");
    }

    /**
     * 使用公钥解密
     *
     * @param string
     * @param key
     * @return
     * @throws Exception
     */
    public static String decryptByPublicKey(String string, String key) throws Exception {
        PublicKey publicKey = getPublicKey(key);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);

        // 由于长度限制,需要分开加密
        byte[] data = Base64.decodeBase64(string);
        int length = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offset = 0;
        byte[] cache;
        int i = 0;
        int blockLength = KEY_SIZE / 8;
        while (length - offset > 0) {
            if (length - offset > blockLength) {
                cache = cipher.doFinal(data, offset, blockLength);
            } else {
                cache = cipher.doFinal(data, offset, length - offset);
            }
            out.write(cache, 0, cache.length);
            i++;
            offset = i * blockLength;
        }

        byte[] bytes = out.toByteArray();
        out.close();

        return new String(bytes, "utf-8");
    }

    /**
     * 去除密钥中的开头和结尾以及换行符,并转成byte[]
     *
     * @param key
     * @return
     */
    private static byte[] initKey(String key) {
        if (key.contains("-----BEGIN PRIVATE KEY-----")) {
            key = key.substring(key.indexOf("-----BEGIN PRIVATE KEY-----") + 27);
        }
        if (key.contains("-----BEGIN PUBLIC KEY-----")) {
            key = key.substring(key.indexOf("-----BEGIN PUBLIC KEY-----") + 26);
        }
        if (key.contains("-----END PRIVATE KEY-----")) {
            key = key.substring(0, key.indexOf("-----END PRIVATE KEY-----"));
        }
        if (key.contains("-----END PUBLIC KEY-----")) {
            key = key.substring(0, key.indexOf("-----END PUBLIC KEY-----"));
        }
        key = key.replaceAll("\r\n", "");
        key = key.replaceAll("\n", "");
        return Base64.decodeBase64(key);
    }
}
复制代码

JwtRsa工具类

@Slf4j
public class JwtRsaUtils {

    /**
     * 提供公钥字符串,返回RSAKey
     *
     * @param keyId
     * @param publicKey
     * @return
     */
    public static RSAKey getRsaKey(String keyId, String publicKey) throws InvalidKeySpecException {
        return getRsaKey(keyId, RsaUtils.getPublicKey(publicKey));
    }

    /**
     * 提供公钥和私钥字符串,返回RSAKey
     *
     * @param keyId
     * @param publicKey
     * @param privateKey
     * @return
     */
    public static RSAKey getRsaKey(String keyId, String publicKey, String privateKey) throws InvalidKeySpecException {
        return getRsaKey(keyId, RsaUtils.getPublicKey(publicKey), RsaUtils.getPrivateKey(privateKey));
    }

    /**
     * 提供公钥,返回RSAKey
     *
     * @param keyId
     * @param publicKey
     * @return
     */
    public static RSAKey getRsaKey(String keyId, PublicKey publicKey) {
        return new RSAKey.Builder((RSAPublicKey) publicKey)
                .keyUse(KeyUse.SIGNATURE)
                .algorithm(JWSAlgorithm.RS256)
                .keyID(keyId)
                .build();
    }

    /**
     * 提供公钥和私钥,返回RSAKey
     *
     * @param keyId
     * @param publicKey
     * @param privateKey
     * @return
     */
    public static RSAKey getRsaKey(String keyId, PublicKey publicKey, PrivateKey privateKey) {
        return new RSAKey.Builder((RSAPublicKey) publicKey)
                .privateKey(privateKey)
                .keyUse(KeyUse.SIGNATURE)
                .algorithm(JWSAlgorithm.RS256)
                .keyID(keyId)
                .build();
    }

    /**
     * 根据RSAKey签名
     *
     * @param rsaKey
     * @return
     * @throws JOSEException
     */
    public static String sign(RSAKey rsaKey) throws JOSEException {
        return sign(rsaKey, new JWTClaimsSet.Builder().build());
    }

    /**
     * 根据RSAKey签名
     *
     * @param rsaKey
     * @param aud
     * @return
     * @throws JOSEException
     */
    public static String sign(RSAKey rsaKey, String... aud) throws JOSEException {
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .audience(Arrays.asList(aud))
                .build();
        return sign(rsaKey, claimsSet);
    }

    /**
     * 根据RSAKey签名,可设置过期时间
     *
     * @param rsaKey
     * @param expire 过期时间,单位毫秒
     * @param aud
     * @return
     * @throws JOSEException
     */
    public static String sign(RSAKey rsaKey, long expire, String... aud) throws JOSEException {
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .audience(Arrays.asList(aud))
                .expirationTime(new Date(System.currentTimeMillis() + expire))
                .build();
        return sign(rsaKey, claimsSet);
    }

    /**
     * 根据RSAKey签名,可设置过期时间
     *
     * @param rsaKey
     * @param issuer  iss
     * @param subject sub
     * @param expire  过期时间,单位毫秒
     * @param aud
     * @return
     * @throws JOSEException
     */
    public static String sign(RSAKey rsaKey, String issuer, String subject, long expire, String... aud) throws JOSEException {
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .issuer(issuer)
                .subject(subject)
                .audience(Arrays.asList(aud))
                .expirationTime(new Date(System.currentTimeMillis() + expire))
                .build();
        return sign(rsaKey, claimsSet);
    }

    /**
     * 签名
     *
     * @param rsaKey
     * @param claimsSet
     * @return
     * @throws JOSEException
     */
    public static String sign(RSAKey rsaKey, JWTClaimsSet claimsSet) throws JOSEException {
        SignedJWT signedJWT = new SignedJWT(
                new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
                claimsSet);
        signedJWT.sign(new RSASSASigner(rsaKey));
        return signedJWT.serialize();
    }

    /**
     * 验证签名
     *
     * @param rsaKey
     * @param token
     * @return
     */
    public static boolean verify(RSAKey rsaKey, String token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);

            if (signedJWT.getJWTClaimsSet().getExpirationTime() != null) {
                if (!new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
                    return false;
                }
            }

            RSASSAVerifier verifier = new RSASSAVerifier(rsaKey);
            return signedJWT.verify(verifier);
        } catch (ParseException e) {
            log.debug("解析JWT失败", e);
            return false;
        } catch (JOSEException e) {
            log.debug("解析JWT时密钥错误", e);
            return false;
        }
    }

    /**
     * 验证签名并返回JWT对象
     *
     * @param rsaKey
     * @param token
     * @return 如果返回值为null就表示验证失败
     */
    public static SignedJWT verifyWithData(RSAKey rsaKey, String token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);

            if (signedJWT.getJWTClaimsSet().getExpirationTime() != null) {
                if (!new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
                    return null;
                }
            }

            RSASSAVerifier verifier = new RSASSAVerifier(rsaKey);
            if (signedJWT.verify(verifier)) {
                return signedJWT;
            } else {
                return null;
            }
        } catch (ParseException e) {
            log.debug("解析JWT失败", e);
            return null;
        } catch (JOSEException e) {
            log.debug("解析JWT时密钥错误", e);
            return null;
        }
    }
}
复制代码

用法

// keyId 是随便填的字符串
RSAKey rsaKey = JwtRsaUtils.getRsaKey(keyId, publicKey, privateKey);
String token = JwtRsaUtils.sign(rsaKey, issuer, subject, expireTime,
        defaultAccount.getId().toString(),
        defaultAccount.getUserId().toString(),
        defaultAccount.getCustomerId().toString(),
        defaultAccount.getName(),
        defaultAccount.getAccountType());

// 直接解析不验证,这一步是不需要密钥的,因为是明文的
SignedJWT jwt = SignedJWT.parse(token);
List<String> audience = jwt.getJWTClaimsSet().getAudience();

// 验证
if (JwtRsaUtils.verify(rsaKey, token)) {
    // 验证通过
}

// 验证并返回payload
SignedJWT jwt = JwtRsaUtils.verifyWithData(rsaKey, token);
if (jwt != null) {
    // 验证通过
    List<String> audience = jwt.getJWTClaimsSet().getAudience();
}
复制代码

公众号:飞翔的代码

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享