Token鉴权
更新时间:
本文描述Token生成方式,您可以了解RTM服务中Token生成方法以及鉴权流程。
鉴权说明
在使用云上曲率RTM服务前,需要对用户进行身份校验。“登录验证Token” 是用户登录RTM的校验凭证,每个用户需要持有合法有效的Token。通过对用户Token的鉴权,用户可以登录RTM并使用RTM服务。
通过业务服务端获取Token
业务客户端如果想要登录RTM,可以通过业务服务端集成RTM服务端SDK,通过RTM服务端SDK与RTM服务器通信,进而获取到Token,从而使业务客户端与RTM服务器通信。
通过业务服务端生成Token
业务客户端如果想要登录RTM,可以直接通过业务服务端自行根据所选的加密方法进行Token的生成,之后将Token给到客户端SDK后,再与RTM服务器通信。
这种方法为业务自行生成Token完成的鉴权。RTM服务器所需的鉴权公钥需要在控制台中填入。
HMAC方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数
类型
说明
pid
int32
项目id
uid
int64
用户id
timestamp
int64
秒级时间戳
- 使用基于sha256的hmac对验证字符串进行hash运算得到签名。
- 对hash值使用base64编码,编码后的字符串即为Token。
在控制台中上传的公钥也是经过base64编码后生成的字符串。
代码示例
- C++
代码需使用openssl库和base64库.
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/err.h>
string genHMACToken(int32_t pid, int64_t uid, int64_t ts, string secret)
{
stringstream ss;
ss << pid << ":" << uid << ":" << ts;
unsigned char hmac[EVP_MAX_MD_SIZE] = {0};
unsigned int len = (secret.size() + 3) / 4 * 3;
unsigned char *key = new unsigned char[len];
memset(key, 0, len);
int result = base64_decode(&std_base64, key, secret.data(), secret.size(), 0);
if (result < 0)
{
cout << "invalid hmac key" << endl;
delete[] key;
return false;
}
HMAC(EVP_sha256(), key, result, (unsigned char *)ss.str().data(), ss.str().size(), hmac, &len);
delete[] key;
char gentoken[64] = {0};
base64_encode(&std_base64, gentoken, hmac, len, 0);
string token = gentoken;
return token;
}
- go
func GenHMACToken(pid int32, uid int64, ts int64, key string) string {
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
keyb := make([]byte, len(key))
kblen, err := base64.RawStdEncoding.Decode(keyb, []byte(key))
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
keyb = keyb[:kblen]
hmacsha256 := hmac.New(func() hash.Hash {
return sha256.New()
}, keyb)
hmacsha256.Write([]byte(content))
res := make([]byte, 0)
res = hmacsha256.Sum(res)
return base64.StdEncoding.EncodeToString(res)
}
- Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class genHMACToken {
public static String genHMACToken(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
byte[] keyb = Base64.getDecoder().decode(secret);
try {
Mac hmacsha256 = Mac.getInstance("HmacSHA256");
Key key = new SecretKeySpec(keyb, "HmacSHA256");
hmacsha256.init(key);
byte[] sig = hmacsha256.doFinal(content.getBytes());
token = new String(Base64.getEncoder().encode(sig));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
return token;
}
}
ECDSA方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数
类型
说明
pid
int32
项目id
uid
int64
用户id
timestamp
int64
秒级时间戳
- 使用基于sha256对验证字符串进行hash运算。
- 对hash值进行ECDSA签名,将签名生成的随机数和签名使用asn1编码后,再进行base64编码得到的字符串即为Token。
代码示例
- C++
string genECDSASign(int32_t pid, int64_t uid, int64_t ts, string privatekey)
{
BIO *privbio = BIO_new_mem_buf(privatekey.c_str(), -1);
if(privbio == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
return false;
}
EVP_PKEY *ekeypriv = PEM_read_bio_PrivateKey(pubbio, NULL, NULL, NULL);
if(evpkey == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
BIO_free_all(privbio);
return false;
}
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
int result = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, ekeypriv);
if (result != 1)
{
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
size_t signlen = 0;
#if OPENSSL_VERSION_NUMBER > 0x10100000
result = EVP_DigestSign(mdctx, NULL, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSign(mdctx, signature, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
#else
EVP_DigestSignUpdate(mdctx, (unsigned char*)ss.str().data(), ss.str().size());
result = EVP_DigestSignFinal(mdctx, NULL, &signlen);
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSignFinal(mdctx, signature, &signlen);
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
#endif
char sign[signlen*4];
memset(sign,0,signlen*4);
base64_encode(&std_base64, sign, signature, signlen, 0);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
string token = sign;
return toekn;
}
- go
type Signature struct {
R *big.Int
S *big.Int
}
func GenECDSAToken(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
pk, err := x509.ParseECPrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
hashsha256 := sha256.New()
hashsha256.Write([]byte(content))
hashres := make([]byte, 0)
hashres = hashsha256.Sum(hashres)
sig := Signature{}
r, s, err := ecdsa.Sign(rand.Reader, pk, hashres)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
sig.R = r
sig.S = s
asn1res, err := asn1.Marshal(sig)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
return base64.StdEncoding.EncodeToString(asn1res)
}
func GenECDSATokenPKCS8(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
tmppk, err := x509.ParsePKCS8PrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
pk := tmppk.(*ecdsa.PrivateKey)
hashsha256 := sha256.New()
hashsha256.Write([]byte(content))
hashres := make([]byte, 0)
hashres = hashsha256.Sum(hashres)
sig := Signature{}
r, s, err := ecdsa.Sign(rand.Reader, pk, hashres)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
sig.R = r
sig.S = s
asn1res, err := asn1.Marshal(sig)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
return base64.StdEncoding.EncodeToString(asn1res)
}
- Java
Java使用 EC PRIVATE KEY 格式的pem文件需导入 bouncycastle库。
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class genECDSAToken {
public static String genECDSAToken(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
Security.addProvider(new BouncyCastleProvider());
PEMReader reader = new PEMReader(new StringReader(secret));
KeyPair keyPair = (KeyPair) reader.readObject();
Signature sigalg = Signature.getInstance("SHA256withECDSA");
sigalg.initSign(keyPair.getPrivate());
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException e) {
e.printStackTrace();
}
return token;
}
public static String genECDSATokenPKCS8(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ec");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("SHA256withECDSA");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
}
控制台上传的公钥
控制台上传的公钥为 pem x509格式的 EC公钥。
示例如下:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmvCgoYs7vfObycAAP4aOmrFw/VKS
KIJgzctUh+2rMQGxaz+/QZNdZBlSGav670grSBJrXhnF7TegXOdHZcXd/w==
-----END PUBLIC KEY-----
在进行ECDSA签名时,需要分辨 openssl 生成的私钥是普通的 EC PRIVATE KEY 格式还是 PKCS8 格式。
- EC PRIVATE KEY 格式
go语言和Java语言直接使上述示例中的方法 genECDSAToken
。特征是 pem文件以-----BEGIN EC PRIVATE KEY -----
开头。
- PKCS8 格式
PKCS8格式的私钥请使用 genECDSATokenPKCS8
方法。特征是 pem文件以-----BEGIN PRIVATE KEY -----
开头。
通过 openssl 生成EC公私钥的方法如下所示:
openssl ecparam -name prime256v1 -genkey -noout -out privatekey.pem
openssl ec -in privatekey.pem -pubout -out publickey.pem
- 私钥默认为 EC PRIVATE KET 。
- PKCS8 格式需要将 EC PRIVATE KET 进行转换:
openssl pkey -in privetekey.pem -out pkcs8.pem
。
EdDSA方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数
类型
说明
pid
int32
项目id
uid
int64
用户id
timestamp
int64
秒级时间戳
- 对字符串进行EdDSA签名,再进行base64编码得到的字符串即为Token。
- EdDSA有两种Ed25519和Ed448两种算法,可以任意选取合适的算法。
代码示例
- C++
string genEdDSASign(int32_t pid, int64_t uid, int64_t ts, string privatekey)
{
BIO *privbio = BIO_new_mem_buf(privatekey.c_str(), -1);
if(privbio == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
return false;
}
EVP_PKEY *ekeypriv = PEM_read_bio_PrivateKey(pubbio, NULL, NULL, NULL);
if(evpkey == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
BIO_free_all(privbio);
return false;
}
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
int result = EVP_DigestSignInit(mdctx, NULL, NULL, NULL, ekeypriv);
if (result != 1)
{
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
size_t signlen = 0;
result = EVP_DigestSign(mdctx, NULL, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSign(mdctx, signature, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
char sign[signlen*4];
memset(sign,0,signlen*4);
base64_encode(&std_base64, sign, signature, signlen, 0);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
string token = sign;
return toekn;
}
- go
go官方库暂时不支持Ed448算法,因此go版本只能使用Ed25519算法。
func GenEd25519Token(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
tmppk, err := x509.ParsePKCS8PrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
pk := tmppk.(ed25519.PrivateKey)
signature := ed25519.Sign(pk, []byte(content))
return base64.StdEncoding.EncodeToString(signature)
}
- Java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class genEdDSAToken {
public static String genEd25519Token(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ed25519");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("ed25519");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
public static String genEd448Token(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ed448");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("ed448");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
}
控制台上传的公钥
控制台上传的公钥为 pem x509格式的 EC公钥。
在进行EdDSA签名时,openssl 生成的私钥均为 PKCS8 格式。
通过 openssl 生成EC公私钥的方法如下所示:
- Ed25519
openssl genpkey -algorithm Ed25519 -out privatekey.pem
openssl pkey -in key.pem -pubout -out pub.pem
- Ed448
openssl genpkey -algorithm Ed448 -out privatekey.pem
openssl pkey -in key.pem -pubout -out pub.pem
- 其中 openssl 需要支持 openssl1.1。
通过控制台获取Token
如果您正处于开发测试阶段,不具备业务服务端资源配合测试,云上曲率提供接口工具,模拟业务服务端请求,获取Token进行业务客户端的测试开发工作。
相关操作请参考:控制台操作->测试与验证
Token有效期
通过业务服务端获取的登录Token最长有效期为24小时,但以下情况可能造成Token提前失效:
- 当同一个用户ID重新获取Token时,旧Token立即失效,使用新的登录Token。
- RTM服务器根据安全需要,动态调整Token的有效期。此时,会强制要求用户在新的有效期到期后重新获取Token。
通过业务服务端生成的登录Token的有效期为24小时,超过有效期后失效。
判断Token失效
在使用RTM客户端SDK进行登录时,如果登录接口执行成功,但输出参数值为 false(一般为ok=false),则代表此时Token已经失效,需要重新从业务服务器获取。
如果您采用业务服务端向RTM服务端获取Token的方法,由于RTM的登录Token有效期在特殊情况下有可能被RTM服务器动态调整,因此请尽量保证业务服务器每次需要时都直接从RTM服务器重新申请,不要增加缓存策略。
本文描述Token生成方式,您可以了解RTM服务中Token生成方法以及鉴权流程。
鉴权说明
在使用云上曲率RTM服务前,需要对用户进行身份校验。“登录验证Token” 是用户登录RTM的校验凭证,每个用户需要持有合法有效的Token。通过对用户Token的鉴权,用户可以登录RTM并使用RTM服务。
通过业务服务端获取Token
业务客户端如果想要登录RTM,可以通过业务服务端集成RTM服务端SDK,通过RTM服务端SDK与RTM服务器通信,进而获取到Token,从而使业务客户端与RTM服务器通信。
通过业务服务端生成Token
业务客户端如果想要登录RTM,可以直接通过业务服务端自行根据所选的加密方法进行Token的生成,之后将Token给到客户端SDK后,再与RTM服务器通信。
这种方法为业务自行生成Token完成的鉴权。RTM服务器所需的鉴权公钥需要在控制台中填入。
HMAC方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数 | 类型 | 说明 |
---|---|---|
pid | int32 | 项目id |
uid | int64 | 用户id |
timestamp | int64 | 秒级时间戳 |
- 使用基于sha256的hmac对验证字符串进行hash运算得到签名。
- 对hash值使用base64编码,编码后的字符串即为Token。
在控制台中上传的公钥也是经过base64编码后生成的字符串。
代码示例
- C++
代码需使用openssl库和base64库.
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/err.h>
string genHMACToken(int32_t pid, int64_t uid, int64_t ts, string secret)
{
stringstream ss;
ss << pid << ":" << uid << ":" << ts;
unsigned char hmac[EVP_MAX_MD_SIZE] = {0};
unsigned int len = (secret.size() + 3) / 4 * 3;
unsigned char *key = new unsigned char[len];
memset(key, 0, len);
int result = base64_decode(&std_base64, key, secret.data(), secret.size(), 0);
if (result < 0)
{
cout << "invalid hmac key" << endl;
delete[] key;
return false;
}
HMAC(EVP_sha256(), key, result, (unsigned char *)ss.str().data(), ss.str().size(), hmac, &len);
delete[] key;
char gentoken[64] = {0};
base64_encode(&std_base64, gentoken, hmac, len, 0);
string token = gentoken;
return token;
}
- go
func GenHMACToken(pid int32, uid int64, ts int64, key string) string {
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
keyb := make([]byte, len(key))
kblen, err := base64.RawStdEncoding.Decode(keyb, []byte(key))
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
keyb = keyb[:kblen]
hmacsha256 := hmac.New(func() hash.Hash {
return sha256.New()
}, keyb)
hmacsha256.Write([]byte(content))
res := make([]byte, 0)
res = hmacsha256.Sum(res)
return base64.StdEncoding.EncodeToString(res)
}
- Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class genHMACToken {
public static String genHMACToken(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
byte[] keyb = Base64.getDecoder().decode(secret);
try {
Mac hmacsha256 = Mac.getInstance("HmacSHA256");
Key key = new SecretKeySpec(keyb, "HmacSHA256");
hmacsha256.init(key);
byte[] sig = hmacsha256.doFinal(content.getBytes());
token = new String(Base64.getEncoder().encode(sig));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
return token;
}
}
ECDSA方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数 | 类型 | 说明 |
---|---|---|
pid | int32 | 项目id |
uid | int64 | 用户id |
timestamp | int64 | 秒级时间戳 |
- 使用基于sha256对验证字符串进行hash运算。
- 对hash值进行ECDSA签名,将签名生成的随机数和签名使用asn1编码后,再进行base64编码得到的字符串即为Token。
代码示例
- C++
string genECDSASign(int32_t pid, int64_t uid, int64_t ts, string privatekey)
{
BIO *privbio = BIO_new_mem_buf(privatekey.c_str(), -1);
if(privbio == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
return false;
}
EVP_PKEY *ekeypriv = PEM_read_bio_PrivateKey(pubbio, NULL, NULL, NULL);
if(evpkey == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
BIO_free_all(privbio);
return false;
}
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
int result = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, ekeypriv);
if (result != 1)
{
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
size_t signlen = 0;
#if OPENSSL_VERSION_NUMBER > 0x10100000
result = EVP_DigestSign(mdctx, NULL, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSign(mdctx, signature, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
#else
EVP_DigestSignUpdate(mdctx, (unsigned char*)ss.str().data(), ss.str().size());
result = EVP_DigestSignFinal(mdctx, NULL, &signlen);
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSignFinal(mdctx, signature, &signlen);
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
#endif
char sign[signlen*4];
memset(sign,0,signlen*4);
base64_encode(&std_base64, sign, signature, signlen, 0);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
string token = sign;
return toekn;
}
- go
type Signature struct {
R *big.Int
S *big.Int
}
func GenECDSAToken(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
pk, err := x509.ParseECPrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
hashsha256 := sha256.New()
hashsha256.Write([]byte(content))
hashres := make([]byte, 0)
hashres = hashsha256.Sum(hashres)
sig := Signature{}
r, s, err := ecdsa.Sign(rand.Reader, pk, hashres)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
sig.R = r
sig.S = s
asn1res, err := asn1.Marshal(sig)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
return base64.StdEncoding.EncodeToString(asn1res)
}
func GenECDSATokenPKCS8(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
tmppk, err := x509.ParsePKCS8PrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
pk := tmppk.(*ecdsa.PrivateKey)
hashsha256 := sha256.New()
hashsha256.Write([]byte(content))
hashres := make([]byte, 0)
hashres = hashsha256.Sum(hashres)
sig := Signature{}
r, s, err := ecdsa.Sign(rand.Reader, pk, hashres)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
sig.R = r
sig.S = s
asn1res, err := asn1.Marshal(sig)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
return base64.StdEncoding.EncodeToString(asn1res)
}
- Java
Java使用 EC PRIVATE KEY 格式的pem文件需导入 bouncycastle库。
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class genECDSAToken {
public static String genECDSAToken(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
Security.addProvider(new BouncyCastleProvider());
PEMReader reader = new PEMReader(new StringReader(secret));
KeyPair keyPair = (KeyPair) reader.readObject();
Signature sigalg = Signature.getInstance("SHA256withECDSA");
sigalg.initSign(keyPair.getPrivate());
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException e) {
e.printStackTrace();
}
return token;
}
public static String genECDSATokenPKCS8(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ec");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("SHA256withECDSA");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
}
控制台上传的公钥
控制台上传的公钥为 pem x509格式的 EC公钥。 示例如下:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmvCgoYs7vfObycAAP4aOmrFw/VKS
KIJgzctUh+2rMQGxaz+/QZNdZBlSGav670grSBJrXhnF7TegXOdHZcXd/w==
-----END PUBLIC KEY-----
在进行ECDSA签名时,需要分辨 openssl 生成的私钥是普通的 EC PRIVATE KEY 格式还是 PKCS8 格式。
- EC PRIVATE KEY 格式
go语言和Java语言直接使上述示例中的方法 genECDSAToken
。特征是 pem文件以-----BEGIN EC PRIVATE KEY -----
开头。
- PKCS8 格式
PKCS8格式的私钥请使用 genECDSATokenPKCS8
方法。特征是 pem文件以-----BEGIN PRIVATE KEY -----
开头。
通过 openssl 生成EC公私钥的方法如下所示:
openssl ecparam -name prime256v1 -genkey -noout -out privatekey.pem
openssl ec -in privatekey.pem -pubout -out publickey.pem
- 私钥默认为 EC PRIVATE KET 。
- PKCS8 格式需要将 EC PRIVATE KET 进行转换:
openssl pkey -in privetekey.pem -out pkcs8.pem
。
EdDSA方法
Token生成流程
- 拼接验证字符串。
- 格式:
pid:uid:timestamp
参数 | 类型 | 说明 |
---|---|---|
pid | int32 | 项目id |
uid | int64 | 用户id |
timestamp | int64 | 秒级时间戳 |
- 对字符串进行EdDSA签名,再进行base64编码得到的字符串即为Token。
- EdDSA有两种Ed25519和Ed448两种算法,可以任意选取合适的算法。
代码示例
- C++
string genEdDSASign(int32_t pid, int64_t uid, int64_t ts, string privatekey)
{
BIO *privbio = BIO_new_mem_buf(privatekey.c_str(), -1);
if(privbio == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
return false;
}
EVP_PKEY *ekeypriv = PEM_read_bio_PrivateKey(pubbio, NULL, NULL, NULL);
if(evpkey == NULL)
{
LOG_ERROR("openssl err:%s", ERR_error_string(ERR_get_error(), NULL));
BIO_free_all(privbio);
return false;
}
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
int result = EVP_DigestSignInit(mdctx, NULL, NULL, NULL, ekeypriv);
if (result != 1)
{
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
size_t signlen = 0;
result = EVP_DigestSign(mdctx, NULL, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
if (result != 1)
{
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
ERR_print_errors_fp(stdout);
return false;
}
unsigned char signature[signlen];
result = EVP_DigestSign(mdctx, signature, &signlen, (unsigned char*)ss.str().data(), ss.str().size());
char sign[signlen*4];
memset(sign,0,signlen*4);
base64_encode(&std_base64, sign, signature, signlen, 0);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(ekeypriv);
BIO_free_all(privbio);
string token = sign;
return toekn;
}
- go
go官方库暂时不支持Ed448算法,因此go版本只能使用Ed25519算法。
func GenEd25519Token(pid int32, uid int64, ts int64, key string) string {
pemKey, _ := pem.Decode([]byte(key))
content := strconv.FormatInt(int64(pid), 10) + ":" + strconv.FormatInt(int64(uid), 10) + ":" + strconv.FormatInt(int64(ts), 10)
tmppk, err := x509.ParsePKCS8PrivateKey(pemKey.Bytes)
if err != nil {
fmt.Printf("error:%s\n", err)
return ""
}
pk := tmppk.(ed25519.PrivateKey)
signature := ed25519.Sign(pk, []byte(content))
return base64.StdEncoding.EncodeToString(signature)
}
- Java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class genEdDSAToken {
public static String genEd25519Token(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ed25519");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("ed25519");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
public static String genEd448Token(int pid, long uid, long ts, String secret) {
String content = pid+":"+uid+":"+ts;
String token = "";
try {
KeyFactory keyFactory = KeyFactory.getInstance("ed448");
BufferedReader reader = new BufferedReader(new StringReader(secret));
String line = reader.readLine();
if (line.compareTo("-----BEGIN PRIVATE KEY-----") != 0)
return token;
String keyBuffer = "";
line = reader.readLine();
while (line.compareTo("-----END PRIVATE KEY-----") != 0)
{
keyBuffer += line;
line = reader.readLine();
}
byte[] pkcs8 = Base64.getDecoder().decode(keyBuffer.getBytes());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pkcs8);
PrivateKey pk = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature sigalg = Signature.getInstance("ed448");
sigalg.initSign(pk);
sigalg.update(content.getBytes());
byte[] sigbytes = sigalg.sign();
token = new String(Base64.getEncoder().encode(sigbytes));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
return token;
}
}
控制台上传的公钥
控制台上传的公钥为 pem x509格式的 EC公钥。
在进行EdDSA签名时,openssl 生成的私钥均为 PKCS8 格式。
通过 openssl 生成EC公私钥的方法如下所示:
- Ed25519
openssl genpkey -algorithm Ed25519 -out privatekey.pem
openssl pkey -in key.pem -pubout -out pub.pem
- Ed448
openssl genpkey -algorithm Ed448 -out privatekey.pem
openssl pkey -in key.pem -pubout -out pub.pem
- 其中 openssl 需要支持 openssl1.1。
通过控制台获取Token
如果您正处于开发测试阶段,不具备业务服务端资源配合测试,云上曲率提供接口工具,模拟业务服务端请求,获取Token进行业务客户端的测试开发工作。 相关操作请参考:控制台操作->测试与验证
Token有效期
通过业务服务端获取的登录Token最长有效期为24小时,但以下情况可能造成Token提前失效:
- 当同一个用户ID重新获取Token时,旧Token立即失效,使用新的登录Token。
- RTM服务器根据安全需要,动态调整Token的有效期。此时,会强制要求用户在新的有效期到期后重新获取Token。
通过业务服务端生成的登录Token的有效期为24小时,超过有效期后失效。
判断Token失效
在使用RTM客户端SDK进行登录时,如果登录接口执行成功,但输出参数值为 false(一般为ok=false),则代表此时Token已经失效,需要重新从业务服务器获取。
如果您采用业务服务端向RTM服务端获取Token的方法,由于RTM的登录Token有效期在特殊情况下有可能被RTM服务器动态调整,因此请尽量保证业务服务器每次需要时都直接从RTM服务器重新申请,不要增加缓存策略。