---
order: 3
---
# 开发数字证书登录令牌

本文讲述如何通过在第三方系统和SuccBI之间传递数字证书令牌来实现第三方系统与SuccBI之间的通信，从而最终实现相互跳转认证，就像访问同一个系统一样。具体有：

- [第三方系统跳转到SuccBI](#server)
- [SuccBI跳转到第三方系统](#client)

## 从第三方系统跳转到SuccBI{#server}

当将SuccBI页面通过iframe嵌入到第三方页面（需要考虑[跨域](./embed-into-3rd.md#cors)）或者作为一个被点击跳转的链接时，希望能在打开SuccBI页面后，能让用户免登录访问SuccBI页面。使用生成数字登录令牌的方式也很容易做到这一点。

开发步骤：

1. 添加一个[证书授信应用](../../sys-settings/security/trustedapps.md#trust-cert)。
2. 准备一个action用于控制重定向到SuccBI。
3. 在action中生成数字证书登录令牌`passport`：
4. 将配置的[证书授信应用](../../sys-settings/security/trustedapps.md#trust-cert)ID作出url `appid`的值，第二步中生成的`passport`作为url参数 `passport` 的值，构建访问SuccBI的的url，最后通过重定向将此url响应给浏览器请求。放在url上的参数需要经过[URL转义](https://tool.chinaz.com/tools/urlencode.aspx)

### 生成数字登录令牌细节说明{#passport}

passport原始字符串格式为：

```bash
userid=USERID&createtime={CREATETIME}&randomStr={RANDOMSTRING}
```

1. 加密过程为，先对原始字符串使用`RSA算法`（加密方式为`PKCS#8`）进行加密，然后再对`RSA加密结果`使用`base64`编码为字符串。
2. 原始字符串中大写为参数，实际使用时请替换为对应的值。
3. userid为需要用户登录的用户ID，需要在[系统中户表](../sys-tables/README.md#users)中有记录，否则用户无法登录。
4. createtime为创建时间，randomStr为随机字符串，这两个是为了保证生成的passport唯一性。
5. base64算法请使用 apache：[commons-codec-1.6.jar](https://mvnrepository.com/artifact/commons-codec/commons-codec/1.6)，不是用jdk内置算法是为了避免jdk提供的base64算法实现不一样，并且使用这个jar的好处在于生成的base64不会换行。
6. `RAS算法`使用`证书私钥`来进行加密。如果使用公钥加密时候会抛出`IOException`，是因为公钥私钥的生成结构有差别。\
   若是在[证书授信应用设置](../../sys-settings/security/trustedapps.md#trust-cert)中的`生成证书`按钮生成证书，弹窗所展示的内容才为`证书私钥`。

### java代码示例{#server-code}

::: details 点击展开查看代码>>

```java
@RequestMapping("/sso")
public void ssoRedirect(String redirect_uri, HttpServletResponse response) throws IOException, GeneralSecurityException {
    String userid = "";  // 请替换为需要登录的用户id，这个用户必须要在 SuccBI 系统表 - SZSYS_5_USERS 中存在有对应的用户数据记录
    String appid = "" ; // 请替换为证书授信应用配置的中的应用ID
    String key = ""; // 请替换为证书的私钥，如果是在 “系统设置” > “授信应用” > “添加证书授信应用” > “生成证书按钮” 生成证书，弹窗所展示内容即为这里需要的证书
    response.sendRedirect(redirect_uri +"?appid=" + appid + "&passport="+ createPassport(userid, key));
}

/**
 * 生成认证信息
 * @param userid 需要登录的用户id
 * @param key 证书的私钥
 * @return
 * @throws IOException
 * @throws GeneralSecurityException JDK 自带： import java.security.GeneralSecurityException;
 */
public String createPassport(String userid, String key) throws IOException, GeneralSecurityException {
    /**
     * 加入创建时间和随机字符串是为了保证唯一性
     */
    String passport = "userid=" + userid + "&createtime=" + System.currentTimeMillis() + "&randomStr=" + (int) (Math.random() * 1000000);
    byte[] passportBytes = passport.getBytes("utf-8");
    // 使用base64 进行解密，base64算法使用的是 apache commons-codec-1.6.jar
    byte[] keyBytes = decryptBASE64(key);
    /**
     *  算法工具来自JDK 自带的
        import java.security.KeyFactory;
        import java.security.spec.PKCS8EncodedKeySpec;
        import java.security.spec.X509EncodedKeySpec;
        import javax.crypto.Cipher;
        */
    PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA", "SunRsaSign");
    Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
    Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm(), "SunJCE");
    cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    // 使用base64 对最终结果进行加密
    // URLEncoder 来自JDK 自带 ：import java.net.URLEncoder;
    return URLEncoder.encode(encryptBASE64(cipher.doFinal(passportBytes)));
}
```

:::

## 从SuccBI跳转访问到第三方系统{#client}

与[从第三方系统跳转到SuccBI](#server)相对应，若是期望在SuccBI中使用iframe嵌入第三系统（需要考虑[跨域](./embed-into-3rd.md#cors)）或者点击链接跳转到第三方系统时，免登录访问第三方系统。也可以同样的使用数字证书登录令牌方式，但是第三方系统需要开发步骤会有些差异：

1. 在SuccBI中生成[系统证书](../../sys-settings/security/trustedapps.md#systemcert)。
2. 准备一个[Filter](https://baike.baidu.com/item/java%E8%BF%87%E6%BB%A4%E5%99%A8/242605)用于过滤出url带有认证信息`appid`和`passport`两个参数的`GET请求`：
3. 在`Filter`中使用[系统证书](../../sys-settings/security/trustedapps.md#systemcert)的证书公钥对参数`passport`进行解密。
4. 使用`passport`解密出来的用户ID登录到系统中，并对请求作出相应。

### 解密数字登录令牌

数字登录令牌`passport`加密方式请参考请参考[生成数字登录令牌细节说明](#passport)。

与之对应，`passport`的解密方式为先使用base64（[commons-codec-1.6.jar](https://mvnrepository.com/artifact/commons-codec/commons-codec/1.6)）解码字符串，然后使用`RSA算法`(加密方式为`PKCS#8`)解密为原始字符串类型：

```bash
userid={USERID}&createtime={CREATETIME}&randomStr={RANDOMSTRING}
```

### java代码示例{#client-code}

::: details 点击展开查看代码>>

```java
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;

@WebFilter("loginFilter")
public class LoginFilter implements Filter {
    private static final String APP_ID = "appid";

    private static final String PASSPORT = "passport";

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String appId = request.getParameter(APP_ID);
        String passport = request.getParameter(PASSPORT);
        if (appId != null && passport != null) {
            String userid = verifyPassport(appId, passport);
            if(userid == null){
                // 解密失败问题处理
            } else {
                /**
                 * 这里尝试使用用户登录到系统中
                 */
                doLogin(userid);
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * 校验传入的appid是否与证书的appid保持一致，若一致则尝试使用证书解码passport，
     *
     * @param appid
     * @param passport
     * @return appid与SuccBI系统证书appid不一致或者解码passport出错时返回null 表明不需要尝试用证书登录
     * 			其他情况返回passport解码后，解析出来的用户ID
     */
    public String verifyPassport(String appid, String passport) throws IOException {
        String key = ""; // 请替换为SuccBI系统证书中证书配置
        String appId = "";// 请替换为SuccBI系统证书的系统ID

        if (!appId.equals(appid)) {
            return null;
        }
        byte[] data = decryptBASE64(passport);
        byte[] keyBytes = decryptBASE64(key);

        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        String outputStr = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA", "SunRsaSign");
            Key publicKey = keyFactory.generatePublic(x509KeySpec);
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm(), "SunJCE");
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            outputStr = new String(cipher.doFinal(data), "utf-8");
        }
        catch (GeneralSecurityException e) {
            e.printStackTrace();
        }

        Map<String, String> param = new HashMap<String, String>();
        String[] arrSplit = null;
        arrSplit = outputStr.split("[&]");
        for (String strSplit : arrSplit) {
            String[] arrSplitEqual = null;
            arrSplitEqual = strSplit.split("[=]");
            if (arrSplitEqual.length > 1) {
                param.put(arrSplitEqual[0], arrSplitEqual[1]);
            }
            else {
                if (arrSplitEqual[0] != "") {
                    param.put(arrSplitEqual[0], "");
                }
            }
        }
        return param.get("userid");
    }

    /**
     * 使用base64解码，base64算法使用的是apache：commons-codec-1.6.jar
     */
    private byte[] decryptBASE64(String key) throws IOException {
        return Base64.decodeBase64(key.getBytes("utf-8"));
    }

    public void destroy() {
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }
}
```

:::
