# 开发数字证书登录令牌
本文讲述如何通过在第三方系统和SuccBI之间传递数字证书令牌来实现第三方系统与SuccBI之间的通信,从而最终实现相互跳转认证,就像访问同一个系统一样。具体有:
# 从第三方系统跳转到SuccBI
当将SuccBI页面通过iframe嵌入到第三方页面(需要考虑跨域)或者作为一个被点击跳转的链接时,希望能在打开SuccBI页面后,能让用户免登录访问SuccBI页面。使用生成数字登录令牌的方式也很容易做到这一点。
开发步骤:
- 添加一个证书授信应用。
- 准备一个action用于控制重定向到SuccBI。
- 在action中生成数字证书登录令牌
passport
: - 将配置的证书授信应用ID作出url
appid
的值,第二步中生成的passport
作为url参数passport
的值,构建访问SuccBI的的url,最后通过重定向将此url响应给浏览器请求。放在url上的参数需要经过URL转义 (opens new window)
# 生成数字登录令牌细节说明
passport原始字符串格式为:
userid=USERID&createtime={CREATETIME}&randomStr={RANDOMSTRING}
- 加密过程为,先对原始字符串使用
RSA算法
(加密方式为PKCS#8
)进行加密,然后再对RSA加密结果
使用base64
编码为字符串。 - 原始字符串中大写为参数,实际使用时请替换为对应的值。
- userid为需要用户登录的用户ID,需要在系统中户表中有记录,否则用户无法登录。
- createtime为创建时间,randomStr为随机字符串,这两个是为了保证生成的passport唯一性。
- base64算法请使用 apache:commons-codec-1.6.jar (opens new window),不是用jdk内置算法是为了避免jdk提供的base64算法实现不一样,并且使用这个jar的好处在于生成的base64不会换行。
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嵌入第三系统(需要考虑跨域)或者点击链接跳转到第三方系统时,免登录访问第三方系统。也可以同样的使用数字证书登录令牌方式,但是第三方系统需要开发步骤会有些差异:
- 在SuccBI中生成系统证书。
- 准备一个Filter (opens new window)用于过滤出url带有认证信息
appid
和passport
两个参数的GET请求
: - 在
Filter
中使用系统证书的证书公钥对参数passport
进行解密。 - 使用
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条评论
评论