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生成流程
  1. 拼接验证字符串。
  • 格式:pid:uid:timestamp
参数 类型 说明
pid int32 项目id
uid int64 用户id
timestamp int64 秒级时间戳
  1. 使用基于sha256的hmac对验证字符串进行hash运算得到签名。
  2. 对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生成流程
  1. 拼接验证字符串。
  • 格式:pid:uid:timestamp
参数 类型 说明
pid int32 项目id
uid int64 用户id
timestamp int64 秒级时间戳
  1. 使用基于sha256对验证字符串进行hash运算。
  2. 对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生成流程
  1. 拼接验证字符串。
  • 格式:pid:uid:timestamp
参数 类型 说明
pid int32 项目id
uid int64 用户id
timestamp int64 秒级时间戳
  1. 对字符串进行EdDSA签名,再进行base64编码得到的字符串即为Token。
  2. 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服务器重新申请,不要增加缓存策略。