# 开发数字证书登录令牌

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

# 从第三方系统跳转到SuccBI

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

开发步骤:

  1. 添加一个证书授信应用
  2. 准备一个action用于控制重定向到SuccBI。
  3. 在action中生成数字证书登录令牌passport
  4. 将配置的证书授信应用ID作出url appid的值,第二步中生成的passport作为url参数 passport 的值,构建访问SuccBI的的url,最后通过重定向将此url响应给浏览器请求。放在url上的参数需要经过URL转义 (opens new window)

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

passport原始字符串格式为:

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

# java代码示例

点击展开查看代码>>
@RequestMapping("/sso")
public void ssoRedirect(String redirect_uri, HttpServletResponse response) throws IOException, GeneralSecurityException {
    String userid = "";  // 请替换为需要登录的用户id,这个用户必须要在 SuccBI 系统表 - SZSYS_4_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跳转访问到第三方系统

从第三方系统跳转到SuccBI相对应,若是期望在SuccBI中使用iframe嵌入第三系统(需要考虑跨域)或者点击链接跳转到第三方系统时,免登录访问第三方系统。也可以同样的使用数字证书登录令牌方式,但是第三方系统需要开发步骤会有些差异:

  1. 在SuccBI中生成系统证书
  2. 准备一个Filter (opens new window)用于过滤出url带有认证信息appidpassport两个参数的GET请求
  3. Filter中使用系统证书的证书公钥对参数passport进行解密。
  4. 使用passport解密出来的用户ID登录到系统中,并对请求作出相应。

# 解密数字登录令牌

数字登录令牌passport加密方式请参考请参考生成数字登录令牌细节说明

与之对应,passport的解密方式为先使用base64(commons-codec-1.6.jar (opens new window))解码字符串,然后使用RSA算法(加密方式为PKCS#8)解密为原始字符串类型:

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

# 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 {
    }
}
是否有帮助?
0条评论
评论