关于使用RSA私钥签名字符串及JWT签名的技术问询
签名问题答疑:字符串签名与JWT签名实现
一、SHA256withRSA/PSS算法签名字符串的正确方式
你的代码存在三个核心问题,导致签名结果和预期不符:
- 私钥格式解析错误:你使用的是
-----BEGIN RSA PRIVATE KEY-----开头的PKCS#1格式私钥,但代码中误用了对应PKCS#8格式的PKCS8EncodedKeySpec,解析逻辑不匹配。 - 签名结果处理错误:直接将签名字节数组转为
String会因编码问题产生乱码,正确做法是对字节数组做Base64编码(JWT场景需用Base64URL编码)。 - RSA/PSS参数未显式指定:默认的
SHA256withRSA/PSS使用的MGF1参数可能与预期不一致,需显式配置哈希算法匹配要求。
修正后的代码:
import java.security.* import java.security.spec.RSAPrivateKeySpec import java.util.Base64 import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.util.io.pem.PemReader import java.io.StringReader fun signStr(strToSign: String): String { val PRIVATE_KEY = """ -----BEGIN RSA PRIVATE KEY----- SOME_PRIVATE_KEY -----END RSA PRIVATE KEY----- """.trimIndent() // 加载BouncyCastle提供器,处理PKCS#1私钥和精细配置PSS参数 Security.addProvider(BouncyCastleProvider()) // 解析PKCS#1格式私钥 val pemReader = PemReader(StringReader(PRIVATE_KEY)) val pemObject = pemReader.readPemObject() val keyFactory = KeyFactory.getInstance("RSA", "BC") val privateKey = keyFactory.generatePrivate(RSAPrivateKeySpec( BigInteger(1, pemObject.content.copyOfRange(0, 128)), BigInteger(1, pemObject.content.copyOfRange(128, 256)) )) // 配置SHA256withRSA/PSS参数,确保MGF1使用SHA256 val pssParams = PSSParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1 ) val signature = Signature.getInstance("SHA256withRSA/PSS", "BC") signature.setParameter(pssParams) signature.initSign(privateKey) // 显式指定UTF-8编码,避免环境编码差异 signature.update(strToSign.toByteArray(Charsets.UTF_8)) val signatureBytes = signature.sign() // 返回Base64编码的签名(JWT场景改用Base64.getUrlEncoder().withoutPadding()) return Base64.getEncoder().encodeToString(signatureBytes) }
说明:
- 引入BouncyCastle是为了便捷处理PKCS#1私钥和PSS参数配置,也可手动将PKCS#1转成PKCS#8格式避免依赖,但依赖BC更省心。
- PSS参数中的32是SHA256摘要长度,1是盐长度(通常与摘要长度一致,可按需调整)。
二、JWT令牌的签名逻辑验证
你的JWT签名框架逻辑正确,但细节存在错误:
- header和payload必须是JSON字符串:JWT的header和payload本质是JSON对象,需先序列化为JSON字符串再做Base64URL编码,不能直接传入任意字符串。
- 签名结果处理顺序错误:应先对签名字节数组做Base64URL编码,而非先转成乱码字符串再编码。
- Base64URL编码需去掉填充:JWT要求使用无填充的Base64URL编码。
修正后的JWT签名代码(基于上述修正后的签名逻辑):
import com.fasterxml.jackson.databind.ObjectMapper import java.util.Base64 import java.security.* import java.security.spec.RSAPrivateKeySpec import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.util.io.pem.PemReader import java.io.StringReader // 定义JWT的header和payload数据类 data class JwtHeader(val alg: String = "PS256", val typ: String = "JWT") data class JwtPayload(val sub: String, val exp: Long) private val objectMapper = ObjectMapper() private val base64UrlEncoder = Base64.getUrlEncoder().withoutPadding() fun signJWT(payload: JwtPayload): String { // 序列化header和payload为JSON字符串 val headerJson = objectMapper.writeValueAsString(JwtHeader()) val payloadJson = objectMapper.writeValueAsString(payload) // 做Base64URL编码 val headerBase64 = base64UrlEncoder.encodeToString(headerJson.toByteArray(Charsets.UTF_8)) val payloadBase64 = base64UrlEncoder.encodeToString(payloadJson.toByteArray(Charsets.UTF_8)) // 拼接待签名字符串 val toSign = "$headerBase64.$payloadBase64" // 签名并做Base64URL编码 val signatureBytes = signStrForJwt(toSign) val signatureBase64 = base64UrlEncoder.encodeToString(signatureBytes) return "$toSign.$signatureBase64" } // 为JWT场景调整签名方法,返回字节数组 fun signStrForJwt(strToSign: String): ByteArray { val PRIVATE_KEY = """ -----BEGIN RSA PRIVATE KEY----- SOME_PRIVATE_KEY -----END RSA PRIVATE KEY----- """.trimIndent() Security.addProvider(BouncyCastleProvider()) val pemReader = PemReader(StringReader(PRIVATE_KEY)) val pemObject = pemReader.readPemObject() val keyFactory = KeyFactory.getInstance("RSA", "BC") val privateKey = keyFactory.generatePrivate(RSAPrivateKeySpec( BigInteger(1, pemObject.content.copyOfRange(0, 128)), BigInteger(1, pemObject.content.copyOfRange(128, 256)) )) val pssParams = PSSParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1 ) val signature = Signature.getInstance("SHA256withRSA/PSS", "BC") signature.setParameter(pssParams) signature.initSign(privateKey) signature.update(strToSign.toByteArray(Charsets.UTF_8)) return signature.sign() }
说明:
- JWT中SHA256withRSA/PSS对应的算法标识是
PS256,header里的alg必须设为该值。 - 可使用Jackson、Gson等任意JSON库完成序列化。
三、推荐的Kotlin/Java库
1. 通用签名场景
- BouncyCastle:Java生态最常用的密码学扩展库,支持几乎所有主流算法和密钥格式,处理RSA/PSS这类算法的参数配置灵活。
- Apache Commons Codec:辅助处理Base64/Base64URL编码,简化编码解码逻辑。
2. JWT场景
- JJWT:轻量级JWT专用库,支持所有标准签名算法(含PS256),封装了JWT的创建、解析、验证全流程,代码简洁易维护。
- Nimbus JOSE + JWT:功能全面的JOSE/JWT库,支持JWK、加密JWT等复杂场景,适合企业级应用。
内容的提问来源于stack exchange,提问作者ProcgerXacker
相关产品推荐
相关产品推荐

