單點(diǎn)登錄
一.功能價(jià)值簡(jiǎn)述
用戶第三方系統(tǒng)與領(lǐng)星實(shí)現(xiàn)登錄集成,實(shí)現(xiàn)雙系統(tǒng)無(wú)縫登錄打通
二.操作說(shuō)明
1、配置單點(diǎn)登錄功能
在【業(yè)務(wù)配置】-【全局】-【單點(diǎn)登錄設(shè)置】中打開(kāi)單點(diǎn)登錄配置對(duì)單點(diǎn)登錄方式進(jìn)行配置。
單點(diǎn)登錄方式:目前僅支持JWT
驗(yàn)簽算法:目前僅支持HS256
驗(yàn)簽密鑰:雙方約定對(duì)稱加密秘鑰,不能低于32個(gè)字符,不能超過(guò)256個(gè)字符
企業(yè)唯一標(biāo)識(shí):進(jìn)行單點(diǎn)登錄時(shí)候校驗(yàn)企業(yè)唯一標(biāo)識(shí)符,通過(guò)此編號(hào)可以識(shí)別你的企業(yè)ID
未映射用戶處理方式:
類型 |
處理規(guī)則 |
創(chuàng)建(默認(rèn)) |
當(dāng)根據(jù)用戶唯一性映射字段,進(jìn)行映射時(shí)候,如果檢測(cè)不到此用戶,則自動(dòng)創(chuàng)建一個(gè)新用戶。 適用于全部用戶在鑒權(quán)系統(tǒng)管理,領(lǐng)星不管理用戶客戶使用 |
禁止登錄 |
當(dāng)映射不到用戶時(shí)候,禁止此用戶登錄系統(tǒng)。 適用于領(lǐng)星有獨(dú)立的一套用戶管理體系,只有創(chuàng)建并配置好映射的用戶能夠?qū)崿F(xiàn)單點(diǎn)登錄。 |
用戶唯一性映射字段:
首次單點(diǎn)登錄時(shí)候,進(jìn)行驗(yàn)證的唯一字段,首次映射之后,會(huì)綁定雙向的唯一ID,后續(xù)變更手機(jī)號(hào)或者真實(shí)姓名都不影響單點(diǎn)登錄功能使用。
-
手機(jī)號(hào)映射:選定手機(jī)號(hào)映射時(shí),手機(jī)號(hào)不允許為空。
-
郵箱映射:選定郵箱映射時(shí),郵箱不允許為空。
-
用戶名映射:選定用戶名映射時(shí),用戶名不允許為空。
2、JWT基本格式
JWT是由三段信息構(gòu)成的,將這三段信息文本用"."鏈接一起就構(gòu)成了JWT字符串。以下為示例
eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ.hnTxa5qkg6HD8xinBjF2VPnfH6WKuhzh8qORYq8ljMI
-
第一部分是header,明文是{"typ":"JWT","alg":"HS256"},經(jīng)過(guò)base64URL編碼之后是eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
-
第二部分是payload,明文是{"sub":"userId001,"exp":"1683517989095","mobile":"10000","email":"lingxingUser@ingxing.com", "username":""lingxingUser,"realname":"領(lǐng)星用戶"} (其中sub是必須字段,mobile/email/username則根據(jù)jwt配置信息中的唯一映射字段填寫(xiě)),經(jīng)過(guò)base64URL編碼之后是eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ
-
第三部分是signature,由第一部分的header+"."+第二部分的payload得到需要簽名的信息是eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ,經(jīng)過(guò)相應(yīng)簽名算法簽名以后(在本例中就是HS256),再通過(guò)base64URL編碼得到簽名是hnTxa5qkg6HD8xinBjF2VPnfH6WKuhzh8qORYq8ljMI以上為jwt生成過(guò)程的基本說(shuō)明,具體細(xì)節(jié)以及實(shí)現(xiàn)庫(kù)可以通過(guò)JWT官網(wǎng)進(jìn)一步了解。
3、JAVA開(kāi)發(fā)實(shí)例
3.1 基于自定義實(shí)現(xiàn)
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.16.0</version>
</dependency>
package com.asinking.cloud.uc.admin.util;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import javax.crypto.Cipher;
import java.net.URLEncoder;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* SSO-JWT第三方系統(tǒng)代碼示例
* </p>
*/
public class SSOJwtUtil {
private static final String PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMl5dXIwbawEOpMY+x02TRMhRXl0Ng4fbF0xofgCRfPkeO9JKU/An4+MtSyXDQNEGh+Eb8e1M2zJ4M7zf3DZ2wBcXn3247xKK8xol8VW2ytuGm5yBvtQpuuwwMTuf4k5rmQj4cMW4Ob2hIumjI4TiwwpEV0CIN4HKsLPXmC2EMsdAgMBAAECgYA/51F0NZ4jqHe3vn2vx1BtF+mEW3LlydvCN4LrOjVb5YTiSO9ch3lUu8mfag3LkmdCxev6iSPVhrbSjXNHpSIMC/xKk/FMjKGXCCsMt8DTzIgOSS8V+7TSr/vxwOOEmQyYxrjdS748dXlAlQ8YslbUxIWO34qFLjpG+YkIb9JvNQJBAO9Ec9P9IQAuslcrhH4Lpph8PHaTdKp+R4xHbx2hbmTdgTg+cYKBTVGec3Caeocr8VgtfSZgPBJGEJi7RNcfXk8CQQDXkGh+dHYli7Sz/Cj4RQzXWAlmaw0Jgmc4eoucj5BGtM85ZtmSKZUXTM3iLidiD8euyOlEIgOL/15o1abNdPDTAkEA7oa5Sd6RZZMn60rQ3K9Ut7Myu6sopUcaoLgeB9YFLby8s4tcsZOhtvpVby4xdEvUX+mJWBacDEOZDAm1CRiWdQJAWpj+0ebwoOcOk3avYWjj9L2zdbAYUp7T8xDODIbqBE2Jqn5ngt6nIpvNC/qJ4tTu/67BGzmQdA5oB3eEG2XCsQJAQinkQqRb42t+yn7AbKm5vx9kFs0r3wlBYihYguM5HU/W2HYS5iyY1r2EmYpaJiHrKGXnND61w3N0k8GSMVD7uA==";
private static final String URL_PARAM = "?authType=jwt¶m=%s";
/**
* TODO 需要替換為客戶訪問(wèn)領(lǐng)星ERP系統(tǒng)的域名,SAAS客戶統(tǒng)一為 https://erp.lingxing.com,獨(dú)立部署客戶需要替換為自己的服務(wù)域名
* 如果需要登錄默認(rèn)進(jìn)入指定菜單,請(qǐng)攜帶具體路徑
* eg: 默認(rèn)進(jìn)入用戶管理頁(yè)面, https://erp.lingxing.com/erp/muser/userManage
*/
private static final String DOMAIN = "https://erp.lingxing.com";
// TODO 需要替換為配置在ERP系統(tǒng)的驗(yàn)簽密鑰字段值 (菜單路徑:設(shè)置/業(yè)務(wù)配置/全局/單點(diǎn)登錄設(shè)置:驗(yàn)簽密鑰)
private static final String SIGN_KEY = "387e98090e064129886e959a94f7aea2";
// TODO 需替換為配置在ERP系統(tǒng)的企業(yè)唯一標(biāo)識(shí)值 (菜單路徑:設(shè)置/業(yè)務(wù)配置/全局/單點(diǎn)登錄設(shè)置:企業(yè)唯一標(biāo)識(shí))
private static final String CLIENT_ID = "4B008083446B05FAE42589CEEEBD611A ";
// TODO 如果需要指定當(dāng)前jwtToken的過(guò)期時(shí)間,請(qǐng)手動(dòng)調(diào)整此項(xiàng)值
public static final Long EXPIRE_TIME = 3600L;//過(guò)期時(shí)間,單位秒,默認(rèn)一小時(shí)
public static void main(String[] args) {
// TODO 用戶信息需替換為本系統(tǒng)的用戶數(shù)據(jù)
String uniqueKey = "userId001";
String mobile = "10000";
String email = "lingxingUser@lingxing.com";
String username = "lingxingUser";
String realname = "領(lǐng)星用戶";
String redirectUrl = getRedirectUrl(uniqueKey, mobile, email, username, realname);
System.out.println("redirectUrl:" + redirectUrl);
}
/**
* 獲取重定向到領(lǐng)星ERP系統(tǒng)的地址
* @param uniqueKey
* @param mobile
* @param email
* @param username
* @param realname
* @return
*/
public static String getRedirectUrl(String uniqueKey, String mobile, String email, String username, String realname ){
String jwtToken = createJWTToken(uniqueKey, mobile, email, username, realname);
String jwtSSOLoginParam = encodeJwtSSOLoginParam(CLIENT_ID, jwtToken);
return DOMAIN + String.format(URL_PARAM, jwtSSOLoginParam);
}
/**
* 加密單點(diǎn)登錄入?yún)?nèi)容
* @param clientId 企業(yè)唯一標(biāo)識(shí)
* @param jwtToken token信息
* @return
*/
private static String encodeJwtSSOLoginParam(String clientId, String jwtToken) {
Map<String, Object> param = new HashMap<>();
param.put("jwtToken", jwtToken);
param.put("clientId", clientId);
return encrypt(JSON.toJSONString(param), PRIVATE_KEY);
}
/**
* 生成JWT-TOKEN
* @return
*/
private static String createJWTToken(String uniqueKey, String mobile, String email, String username, String realname) {
Map<String, Object> header = new HashMap<>();
header.put("type", "JWT");
header.put("alg","HS256");
Date exp = new Date(new Date().getTime() + EXPIRE_TIME * 1000);
return JWT.create()
.withHeader(header)
.withSubject(uniqueKey)
.withExpiresAt(exp)
.withClaim("mobile", mobile)
.withClaim("email", email)
.withClaim("username", username)
.withClaim("realname", realname)
.withClaim("timestamp", System.currentTimeMillis())
.sign(Algorithm.HMAC256(SIGN_KEY));
}
/**
* RSA加密 - 私鑰分段加密
* @param content
* @param privateKeyStr
* @return
*/
private static String encrypt(String content, String privateKeyStr) {
try {
// 獲取私鑰
PrivateKey privateKey = getPrivateKey(privateKeyStr);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// URLEncoder編碼解決中文亂碼問(wèn)題
byte[] data = URLEncoder.encode(content, "UTF-8").getBytes("UTF-8");
// 加密時(shí)超過(guò)117字節(jié)就報(bào)錯(cuò)。為此采用分段加密的辦法來(lái)加密
byte[] enBytes = null;
for (int i = 0; i < data.length; i += 117) {
// 注意要使用2的倍數(shù),否則會(huì)出現(xiàn)加密后的內(nèi)容再解密時(shí)為亂碼
byte[] doFinal = cipher.doFinal(subarray(data, i, i + 117));
enBytes = addAll(enBytes, doFinal);
}
return Base64.getEncoder().encodeToString(enBytes).replaceAll("\\+", "-").replaceAll("/", "_").replaceAll("=", "");
} catch(Exception e) {
throw new RuntimeException("UC-RSA加密出錯(cuò)");
}
}
/**
* 將base64編碼后的私鑰字符串轉(zhuǎn)成PrivateKey實(shí)例
* @param privateKey 私鑰
* @return PrivateKey實(shí)例
* @throws Exception 異常信息
*/
private static PrivateKey getPrivateKey(String privateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
private static byte[] subarray(byte[] array, int startIndexInclusive, int endIndexExclusive) {
if (array == null) {
return null;
} else {
if (startIndexInclusive < 0) {
startIndexInclusive = 0;
}
if (endIndexExclusive > array.length) {
endIndexExclusive = array.length;
}
int newSize = endIndexExclusive - startIndexInclusive;
if (newSize <= 0) {
return new byte[0];
} else {
byte[] subarray = new byte[newSize];
System.arraycopy(array, startIndexInclusive, subarray, 0, newSize);
return subarray;
}
}
}
private static byte[] addAll(byte[] array1, byte... array2) {
if (array1 == null) {
return clone(array2);
} else if (array2 == null) {
return clone(array1);
} else {
byte[] joinedArray = new byte[array1.length + array2.length];
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
return joinedArray;
}
}
private static byte[] clone(byte[] array) {
return array == null ? null : (byte[])array.clone();
}
}
3. 用戶需要替換的變量
變量名稱 |
是否必填 |
變量說(shuō)明 |
值來(lái)源 |
DOMAIN |
是 |
訪問(wèn)ERP系統(tǒng)的域名 (例如: http://erp.lingxing.com) |
客戶訪問(wèn)ERP系統(tǒng)的地址 |
SIGN_KEY |
是 |
驗(yàn)簽密鑰 |
客戶在ERP系統(tǒng)配置內(nèi)容 |
CLIENT_ID |
是 |
企業(yè)唯一標(biāo)識(shí) |
客戶在ERP系統(tǒng)配置內(nèi)容 |
EXPIRE_TIME |
否 |
jwt-token過(guò)期時(shí)間(默認(rèn)3600秒) |
客戶自定義 |
uniqueKey |
是 |
客戶企業(yè)用戶唯一標(biāo)識(shí)(不可變) |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
mobile |
否 |
用戶手機(jī)號(hào) |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
|
否 |
用戶郵箱 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
username |
否 |
用戶名 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
realname |
否 |
真實(shí)姓名 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
注意事項(xiàng):
1、使用企業(yè)用戶唯一標(biāo)識(shí)字段(uniqueKey):建議設(shè)置為您系統(tǒng)的唯一且不可變的字段,例如用戶ID。用戶初次映射成功后,后續(xù)變更了用戶基本信息(手機(jī)號(hào)/郵箱/用戶名),依舊會(huì)通過(guò)獲取初次映射的uniqueKey對(duì)應(yīng)的用戶數(shù)據(jù)進(jìn)行單點(diǎn)登錄。
2、客戶基本信息(mobile/email/username): 當(dāng)企業(yè)JWT配置中設(shè)置手機(jī)號(hào)為用戶唯一性映射字段時(shí),手機(jī)號(hào)便不允許為空,其余字段同理。
3.2 基于領(lǐng)星SDK實(shí)現(xiàn)
1、pom引入JWT依賴包
<dependency>
<groupId>com.asinking.cloud</groupId>
<artifactId>lingxing-sso-sdk</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
2、Java示例代碼
package com.asinking.uc.sso;
import com.asinking.uc.sso.bean.JwtSSOLoginDTO;
import com.asinking.uc.sso.exception.SSOLoginException;
import com.asinking.uc.sso.service.JWTLoginClient;
public class Main {
public static void main(String[] args){
JWTLoginClient client = new JWTLoginClient();
JwtSSOLoginDTO loginDTO = JwtSSOLoginDTO.builder()
.domain("http://erp.lingxing.com")
.signKey("c53cd1b05f494402a3727019f2dee028")
.clientId("1FA748ECE9921E4EC26CD6540E90DE6C")
.expireMilliseconds(3600000L)
.uniqueKey("10000000")
.mobile("10000")
.email("lingxingerp@lingxing.com")
.username("lingxingerp")
.realName("領(lǐng)星ERP用戶")
.build();
String redirectUrl = null;
try {
redirectUrl = client.getRedirectUrl(loginDTO);
} catch (SSOLoginException e) {
// TODO 異常處理
}
System.out.println(redirectUrl);
}
}
3. 客戶需要替換的變量
變量名稱 |
是否必填 |
變量說(shuō)明 |
值來(lái)源 |
domain |
是 |
訪問(wèn)ERP系統(tǒng)的域名 (例如: http://erp.lingxing.com) |
客戶訪問(wèn)ERP系統(tǒng)的地址 |
signKey |
是 |
驗(yàn)簽密鑰 |
客戶在ERP系統(tǒng)配置內(nèi)容 |
clientId |
是 |
企業(yè)唯一標(biāo)識(shí) |
客戶在ERP系統(tǒng)配置內(nèi)容 |
expireMilliseconds |
否 |
jwt-token過(guò)期時(shí)間(默認(rèn)3600秒) |
客戶自定義 |
uniqueKey |
是 |
客戶企業(yè)用戶唯一標(biāo)識(shí)(不可變) |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
mobile |
否 |
用戶手機(jī)號(hào) |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
|
否 |
用戶郵箱 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
username |
否 |
用戶名 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
realname |
否 |
真實(shí)姓名 |
客戶系統(tǒng)內(nèi)部用戶數(shù)據(jù) |
4、單點(diǎn)登錄領(lǐng)星ERP系統(tǒng)
打開(kāi)新的瀏覽器頁(yè)簽,單點(diǎn)登錄領(lǐng)星ERP系統(tǒng)(需要更換為自己的域名,以下為SAAS登錄示例)
http://erp.lingxing.com?authType=jwt¶m={param}
示例:
http://erp.lingxing.com?authType=jwt¶m=NmI79VpmUjPY0DYCTulCv-HhNqfEKVt163Xd5PNa-CC20eNMj_mmPXcpfLe_c5eFTAfaQkYVxavKFjn0zqHJNi7xFaX21e_T3Or5zjH6KOB8uROzTGOQFoWdZRc9zu15bbNgic9xZUjZTFFH8s-h6766-RjGQ0dtAJQi56MI60Zwejco1lqXlPaMaJShbbY2i49cwvyYpbsGKYd13_W2icM6iz96i6slaES4HBaB6ok7-Ilg1D5t-jW8w5JqGD3NCyK5kpVvOSxsyweoJXbqWukc_gvWNDqP0OE6-wnzuYJvnExzwA7Fyxg5VcXoe4QgVwEfLiGIy3k7l_8YpkrKfQulVYccwDZxtyqaW3dMyAaJzg-ID-Xmz6q2OhtJ-xaxKO8MWaHmnPYhtNJdFHDWPmszPdBjiIOBh-zEhgS5xhovlT-ZXgktG0pqegfZRwuMlmTlpB226fQ3YvvMP_qs13Zc_aki45QCKXVlYUdYRhYgtPlnww4SO74Gl2jRZDkr
未能解決你的問(wèn)題?請(qǐng)聯(lián)系在線客服
請(qǐng)問(wèn)有什么疑問(wèn)?
請(qǐng)問(wèn)有什么疑問(wèn)?