目前已知的单点登陆方式有:
多个系统集群
建立一个SSO认证中心,用户只需要登录一次就可以访问所有相互信任的应用系统。
1、可以通过session广播机制实现:在一个集群中的一个模块登录后,然后把这个session复制n份,发送到这个集群的其他模块中,就实现了一处登录,处处可用,但缺点是耗费比较大,不推荐使用
2、使用cookie+redis实现:在项目中任何一个模块登录,登录之后,把数据放到这两个地方
(1)redis:在key:生成唯一随机值(ip、用户id等等) ,在value:用户数据
(2)cookie:把redis里面生成key值放到cookie里面
点对点系统
3、使用token实现
token:按照一定规则生成字符串,字符串可以包含用户信息的令牌
在项目某个模块进行登录,登录之后,按照规则生成字符串,把登陆之后用户包含到生成字符串里面,把字符串返回
(1)可以把字符串通过cookie返回
(2)把字符串通过地址栏返回
2.再去访问项目其他模块,每次访问在地址栏带着生成的字符串,在访问模块里面获取地址字符串,根据字符串获取用户信息。如果可以获取到,就是登录
基于公私密钥的单点登录
针对两个系统之间的单点登陆,使用token类似方式实现是最简单的。这里,我们通过一定的规则构造字符串(必须带有用户账户信息),并根据已有的私钥进行加密签名,返回到url中,请求目标系统,目标系统解析url中的签名,并根据公钥进行验签,通过之后,允许登陆,写入登陆信息到session。
公私秘钥的安全性由算法保证。算法是基于DSA签名, PKCS #8和X509是对应算法的处理类。对私钥按照 PKCS #8 标准编码处理、对公钥是使用了X509。
DSA算法概述
DSA算法是美国的国家标准数字签名算法,它只能用户数字签名,而不能用户数据加密和密钥交换。
DSA与RSA的生成方式不同,RSA是使用openssl提供的指令一次性的生成密钥(包括公钥),而通常情况下,DSA是先生成DSA的密钥参数,然后根据密钥参数生成DSA密钥(包括公钥),密钥参数决定了DSA密钥的长度,而且一个密钥参数可以生成多对DSA密钥对。
DSA生成的密钥参数是p、q和g,如果要使用一个DSA密钥,需要首先共享其密钥参数。关于DSA加密的原理,请自行查阅。
私钥、公钥的获取可以使用OpenSSL工具生成,文章:https://www.jianshu.com/p/35ae4fb9e4a3
生成加密密钥部分参考
private static final String DSA = "DSA";
private static SimpleDateFormat CRED_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmm");
private static String CRED_SEPERATOR = ":";
private static String signatureAlg = "SHA1WithDSA";
private static String SELF_PROJECTENAME = "A";
private static String privateKeyStr = "******";
private static String publicKeyStr = "*****"
KeyFactory keyFactory = KeyFactory.getInstance(DSA);
//对私钥解密
byte pri[] = ByteUtils.decodeHex(privateKeyStr);
//取私钥
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(pri);
PrivateKey priKey = keyFactory.generatePrivate(privKeySpec);Calendar now = Calendar.getInstance();
String minuteStr = CRED_TIME_FORMAT.format(now.getTime());
StringBuffer str = new StringBuffer();
//增加判定的其他参数
str.append(minuteStr).append(CRED_SEPERATOR).append(user).append(
CRED_SEPERATOR).append(self).append(CRED_SEPERATOR).append(
target);
//对数据签名
Signature sig = Signature.getInstance(signatureAlg);
sig.initSign(priKey);
sig.update(str.toString().getBytes("UTF8"));
byte[] signature = sig.sign();
String signatureStr = ByteUtils.encodeHex(signature);
str.append(CRED_SEPERATOR).append(signatureStr);
解析加密密钥部分参考
KeyFactory keyFactory = KeyFactory.getInstance(DSA);
//对公钥解密
//解析步骤:1 获取p_password参数,检查参数格式是否正确
// 2 将p_password拆解为两部分,最后一个:前一部分是signTarget单点信息摘要,后一部分是signatureTxt签名
// 3 设置算法signature参数,包括公钥,信息摘要和签名,调用验证,返回是否验证通过的结果ok
String[] segments = cre.split(CRED_SEPERATOR);
if (segments.length != 5) {
logger.warn("票据应该是5段");
return null;
}
//时间
String ticketTime = segments[0];
//登录用户名
String ssoUser = segments[1];
//源系统
String srcSystem = segments[2];
//目标系统
String targetSystem = segments[3];
//私钥签名
String signatureTxt = segments[4];
String signTarget = cre.substring(0, cre.lastIndexOf(CRED_SEPERATOR));
byte pub[] = ByteUtils.decodeHex(publicKeyStr);
//取公钥
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pub);
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
byte[] signature = ByteUtils.decodeHex(signatureTxt);
Signature sig = Signature.getInstance(signatureAlg);
sig.initVerify(pubKey);
sig.update(signTarget.getBytes("UTF8"));
//验证签名
boolean ok = sig.verify(signature);
工具类ByteUtil
public class ByteUtils {
/**
* 转换md5码
*
* @param data 要转换的String
* @return md5 md5码
*/
public static final String MD5(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(data.getBytes("utf-8"));
return encodeHex(digest.digest());
} catch (Exception e) {
throw new IllegalStateException(e.getMessage());
}
}
/**
* 将bytes字节转换成md5码
*
* @param bytes bytes
* @return the string
*/
public static final String encodeHex(byte bytes[]) {
StringBuffer buf = new StringBuffer(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
if ((bytes[i] & 0xff) < 16)
buf.append("0");
buf.append(Long.toString(bytes[i] & 0xff, 16));
}
return buf.toString();
}
/**
* 字符串编码转为byte字节数组
*
* @param hex 要转换的字符串
* @return byte[] 转换得到的byte编码
*/
public static final byte[] decodeHex(String hex) {
char chars[] = hex.toCharArray();
byte bytes[] = new byte[chars.length / 2];
int byteCount = 0;
for (int i = 0; i < chars.length; i += 2) {
int newByte = 0;
newByte |= hexCharToByte(chars[i]);
newByte <<= 4;
newByte |= hexCharToByte(chars[i + 1]);
bytes[byteCount] = (byte) newByte;
byteCount++;
}
return bytes;
}
/**
*
*/
private static final byte hexCharToByte(char ch) {
switch (ch) {
case 48: // '0'
return 0;
case 49: // '1'
return 1;
case 50: // '2'
return 2;
case 51: // '3'
return 3;
case 52: // '4'
return 4;
case 53: // '5'
return 5;
case 54: // '6'
return 6;
case 55: // '7'
return 7;
case 56: // '8'
return 8;
case 57: // '9'
return 9;
case 97: // 'a'
return 10;
case 98: // 'b'
return 11;
case 99: // 'c'
return 12;
case 100: // 'd'
return 13;
case 101: // 'e'
return 14;
case 102: // 'f'
return 15;
case 58: // ':'
case 59: // ';'
case 60: // '<'
case 61: // '='
case 62: // '>'
case 63: // '?'
case 64: // '@'
case 65: // 'A'
case 66: // 'B'
case 67: // 'C'
case 68: // 'D'
case 69: // 'E'
case 70: // 'F'
case 71: // 'G'
case 72: // 'H'
case 73: // 'I'
case 74: // 'J'
case 75: // 'K'
case 76: // 'L'
case 77: // 'M'
case 78: // 'N'
case 79: // 'O'
case 80: // 'P'
case 81: // 'Q'
case 82: // 'R'
case 83: // 'S'
case 84: // 'T'
case 85: // 'U'
case 86: // 'V'
case 87: // 'W'
case 88: // 'X'
case 89: // 'Y'
case 90: // 'Z'
case 91: // '['
case 92: // '\\'
case 93: // ']'
case 94: // '^'
case 95: // '_'
case 96: // '`'
default:
return 0;
}
}