技术文章
单点登录(Single Sign On),简称为 SSO,定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
O2OA系统默认提供两种单点登陆解决方案:加密认证方式和OAuth2认证方式。本文主要介绍加密认证方式。有关OAuth2认证方式请查看:https://www.o2oa.net/cms/tech/102.html
加密认证方案是第三方系统通过生成加密的临时票据,然后使用这个临时票据进行单点认证。
要产生认证临时票据,我们需要先在O2OA平台创建一个鉴权密钥配置。
具有管理员权限的用户(具有Manager角色),打开“系统设置”-“安全配置”-“单点登录”,找到“鉴权密钥配置”。
点击“添加鉴权配置”:
此处需要配置一个名称和密钥。
名称:可随意填写,就是我们在调用接口或进行SSO时要传入的client参数。
密钥:可随意填写,用于后续加密,最少8位。
此处我们假设名称填写:oa;密钥填写:platform
填写完成后确定。
然后我们就可以使用此鉴权配置来实现单点登录了。
将创建好的鉴权名称和鉴权密钥告知外部系统,由外部系统生成与O2OA单点认证的临时票据。
外部系统必须先生成临时票据(token),这个token是采取DES算法使用密钥对"person#timestamp"文本进行加密获取的。其中:
person:表示指定用户的用户名、唯一编码或员工号。(具体使用哪个要根据外部系统与O2OA的用户关联的字段)
timestamp:表示为1970年1月1日0时0秒到当前时间的毫秒数。(token的有效时间为1分钟)
下面提供一些加密的样例代码:
我们使用CryptoJS进行DES加密。(GitHub: https://github.com/brix/crypto-js)
样例:
var login_uid = "test"; var time = new Date().getTime(); var sso_key = "12345678"; var xtoken = crypDES( login_uid + "#" + time, sso_key );
/// o2oa DES加密 @param publicKey 加密公钥 func o2DESEncode(code: String, publicKey: String) -> String? { if let encode = desEncrypt(code: code, key: publicKey, iv: "12345678", options: (kCCOptionECBMode + kCCOptionPKCS7Padding)) { let first = encode.replacingOccurrences(of: "+", with: "-") let second = first.replacingOccurrences(of: "/", with: "_") let token = second.replacingOccurrences(of: "=", with: "") return token }else { print("加密错误") return nil } } /// DES 加密 func desEncrypt(code: String, key:String, iv:String, options:Int = kCCOptionPKCS7Padding) -> String? { if let keyData = key.data(using: String.Encoding.utf8), let data = code.data(using: String.Encoding.utf8), let cryptData = NSMutableData(length: Int((data.count)) + kCCBlockSizeDES) { let keyLength = size_t(kCCKeySizeDES) let operation: CCOperation = UInt32(kCCEncrypt) let algoritm: CCAlgorithm = UInt32(kCCAlgorithmDES) let options: CCOptions = UInt32(options) var numBytesEncrypted :size_t = 0 let cryptStatus = CCCrypt(operation, algoritm, options, (keyData as NSData).bytes, keyLength, iv, (data as NSData).bytes, data.count, cryptData.mutableBytes, cryptData.length, &numBytesEncrypted) if UInt32(cryptStatus) == UInt32(kCCSuccess) { cryptData.length = Int(numBytesEncrypted) let base64cryptString = cryptData.base64EncodedString() return base64cryptString } else { return nil } } return nil }
样例:
let uid = "test" let timeInterval = Date().timeIntervalSince1970 let time = CLongLong(round(timeInterval*1000)) let code = "(uid)#(time)" let xtoken = code.o2DESEncode() ?? ""
fun o2DESEncode(code: String, publicKey: String): String { val sutil = CryptDES.getInstance(publicKey) var encode = "" try { encode = sutil.encryptBase64(code) Log.d(LOG_TAG,"加密后code:$encode") encode = encode.replace("+", "-") encode = encode.replace("/", "_") encode = encode.replace("=", "") Log.d(LOG_TAG,"替换特殊字符后的code:$encode") }catch (e: Exception) { Log.e(LOG_TAG,"加密失败", e) } return encode } public class CryptDES { private Cipher encryptCipher = null; private Cipher decryptCipher = null; private static CryptDES des = null; public static CryptDES getInstance(String des_key) { try { DESKeySpec key = new DESKeySpec(des_key.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); des = new CryptDES(keyFactory.generateSecret(key)); } catch (Exception e) { e.printStackTrace(); } return des; } private CryptDES(SecretKey key) throws Exception { encryptCipher = Cipher.getInstance("DES"); decryptCipher = Cipher.getInstance("DES"); encryptCipher.init(Cipher.ENCRYPT_MODE, key); decryptCipher.init(Cipher.DECRYPT_MODE, key); } public String encryptBase64 (String unencryptedString) throws Exception { // Encode the string into bytes using utf-8 byte[] unencryptedByteArray = unencryptedString.getBytes("UTF8"); // Encrypt byte[] encryptedBytes = encryptCipher.doFinal(unencryptedByteArray); // Encode bytes to base64 to get a string byte [] encodedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT); return new String(encodedBytes); } public String decryptBase64 (String encryptedString) throws Exception { // Encode bytes to base64 to get a string byte [] decodedBytes = Base64.encode(encryptedString.getBytes(), Base64.DEFAULT); // Decrypt byte[] unencryptedByteArray = decryptCipher.doFinal(decodedBytes); // Decode using utf-8 return new String(unencryptedByteArray, "UTF8"); } }
Crypto.java import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; /** * encrypt and decrypt utils * @author O2OA * */ public class Crypto { private static final String utf8 = "UTF-8"; private final static String DES = "DES"; private final static String cipher_init = "DES"; public static String encrypt(String data, String key) throws Exception { byte[] bt = encrypt(data.getBytes(), key.getBytes()); String str = Base64.encodeBase64URLSafeString(bt); return URLEncoder.encode( str, utf8 ); } public static byte[] encrypt(byte[] data, byte[] key) throws Exception { // 生成一个可信任的随机数源 SecureRandom sr = new SecureRandom(); // 从原始密钥数据创建DESKeySpec对象 DESKeySpec dks = new DESKeySpec(key); // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); SecretKey securekey = keyFactory.generateSecret(dks); // Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(cipher_init); // 用密钥初始化Cipher对象 cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); return cipher.doFinal(data); } public static String decrypt(String data, String key) throws IOException, Exception { if (StringUtils.isEmpty(data)) { return null; } String str = URLDecoder.decode(data, utf8); byte[] buf = Base64.decodeBase64(str); byte[] bt = decrypt(buf, key.getBytes()); return new String(bt); } public static byte[] decrypt(byte[] data, byte[] key) throws Exception { // 生成一个可信任的随机数源 SecureRandom sr = new SecureRandom(); // 从原始密钥数据创建DESKeySpec对象 DESKeySpec dks = new DESKeySpec(key); // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); SecretKey securekey = keyFactory.generateSecret(dks); // Cipher对象实际完成解密操作 Cipher cipher = Cipher.getInstance(cipher_init); // 用密钥初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, sr); return cipher.doFinal(data); } }
调用:
String login_uid = "test"; long time = new Date().getTime(); String sso_key = "12345678"; String xtoken = Crypto.encrypt( login_uid + "#" + time, sso_key );
生成token后,外部系统可以直接通过访问以下地址,实现与O2OA的单点认证:
http://servername/x_desktop/sso.html?client={client}&xtoken={token}&redirect={redirect}
其中的client表示使用的鉴权名称;
token表示产生的临时票据token;
redirect表示认证成功后要跳转到的地址;
当然也可以通过访问下面的接口服务,来获取O2OA平台的登录session:
getLogin: x_organization_assemble_authentication 下的SsoAction中的getLogin方法
postLogin: x_organization_assemble_authentication 下的SsoAction中的postLogin方法