基于公私密钥的单点登录
目前已知的单点登陆方式有:
多个系统集群
建立一个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参数,包括公钥,信息摘要和签名,调用验证,返回是否验证通过的结果okString[] 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;}}