单点登录
一.功能价值简述
用户第三方系统与领星实现登录集成,实现双系统无缝登录打通
二.操作说明
1、配置单点登录功能
在业务配置-全局-单点登录设置中打开单点登录配置对单点登录方式进行配置。

单点登录方式:目前仅支持JWT
验签算法:目前仅支持HS256
验签密钥:双方约定对称加密秘钥,不能低于32个字符,不能超过256个字符
企业唯一标识:进行单点登录时候校验企业唯一标识符,通过此编号可以识别你的企业ID
未映射用户处理方式:
| 
 类型  | 
 处理规则  | 
| 
 创建(默认)  | 
 当根据用户唯一性映射字段,进行映射时候,如果检测不到此用户,则自动创建一个新用户。 适用于全部用户在鉴权系统管理,领星不管理用户客户使用  | 
| 
 禁止登录  | 
 当映射不到用户时候,禁止此用户登录系统。 适用于领星有独立的一套用户管理体系,只有创建并配置好映射的用户能够实现单点登录。  | 
用户唯一性映射字段:
首次单点登录时候,进行验证的唯一字段,首次映射之后,会绑定双向的唯一ID,后续变更手机号或者真实姓名都不影响单点登录功能使用。
- 
手机号映射:选定手机号映射时,手机号不允许为空。
 - 
邮箱映射:选定邮箱映射时,邮箱不允许为空。
 - 
用户名映射:选定用户名映射时,用户名不允许为空。
 
2、JWT基本格式
JWT是由三段信息构成的,将这三段信息文本用"."链接一起就构成了JWT字符串。以下为示例
eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ.hnTxa5qkg6HD8xinBjF2VPnfH6WKuhzh8qORYq8ljMI
- 
第一部分是header,明文是{"typ":"JWT","alg":"HS256"},经过base64URL编码之后是eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
 - 
第二部分是payload,明文是{"sub":"userId001,"exp":"1683517989095","mobile":"10000","email":"lingxingUser@ingxing.com", "username":""lingxingUser,"realname":"领星用户"} (其中sub是必须字段,mobile/email/username则根据jwt配置信息中的唯一映射字段填写),经过base64URL编码之后是eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ
 - 
第三部分是signature,由第一部分的header+"."+第二部分的payload得到需要签名的信息是eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ1c2VySWQwMDEiLCJtb2JpbGUiOiIxMDAwIiwiZXhwIjoxNjgzNTE3OTg5LCJlbWFpbCI6Imxpbmd4aW5nVXNlckBpbmd4aW5nLmNvbSIsInVzZXJuYW1lIjoibGluZ3hpbmdVc2VyIiwicmVhbG5hbWUiOiLpoobmmJ_nlKjmiLcifQ,经过相应签名算法签名以后(在本例中就是HS256),再通过base64URL编码得到签名是hnTxa5qkg6HD8xinBjF2VPnfH6WKuhzh8qORYq8ljMI以上为jwt生成过程的基本说明,具体细节以及实现库可以通过JWT官网进一步了解。
 
3、JAVA开发实例
3.1 基于自定义实现
<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第三方系统代码示例
 * </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 需要替换为客户访问领星ERP系统的域名,SAAS客户统一为 https://erp.lingxing.com,独立部署客户需要替换为自己的服务域名
     * 如果需要登录默认进入指定菜单,请携带具体路径
     * eg: 默认进入用户管理页面, https://erp.lingxing.com/erp/muser/userManage
     */
    private static final String DOMAIN = "https://erp.lingxing.com";
    // TODO 需要替换为配置在ERP系统的验签密钥字段值 (菜单路径:设置/业务配置/全局/单点登录设置:验签密钥)
    private static final String SIGN_KEY = "387e98090e064129886e959a94f7aea2";
    // TODO 需替换为配置在ERP系统的企业唯一标识值 (菜单路径:设置/业务配置/全局/单点登录设置:企业唯一标识)
    private static final String CLIENT_ID = "4B008083446B05FAE42589CEEEBD611A ";
    // TODO 如果需要指定当前jwtToken的过期时间,请手动调整此项值
    public static final Long EXPIRE_TIME = 3600L;//过期时间,单位秒,默认一小时
    public static void main(String[] args) {
        // TODO 用户信息需替换为本系统的用户数据
        String uniqueKey = "userId001";
        String mobile = "10000";
        String email = "lingxingUser@lingxing.com";
        String username = "lingxingUser";
        String realname = "领星用户";
        String redirectUrl = getRedirectUrl(uniqueKey, mobile, email, username, realname);
        System.out.println("redirectUrl:" + redirectUrl);
    }
    /**
     * 获取重定向到领星ERP系统的地址
     * @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);
    }
    /**
     * 加密单点登录入参内容
     * @param clientId 企业唯一标识
     * @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编码解决中文乱码问题
            byte[] data = URLEncoder.encode(content, "UTF-8").getBytes("UTF-8");
            // 加密时超过117字节就报错。为此采用分段加密的办法来加密
            byte[] enBytes = null;
            for (int i = 0; i < data.length; i += 117) {
                // 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码
                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加密出错");
        }
    }
    /**
     * 将base64编码后的私钥字符串转成PrivateKey实例
     * @param privateKey 私钥
     * @return PrivateKey实例
     * @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. 用户需要替换的变量
| 
 变量名称  | 
 是否必填  | 
 变量说明  | 
 值来源  | 
| 
 DOMAIN  | 
 是  | 
 访问ERP系统的域名 (例如: http://erp.lingxing.com)  | 
 客户访问ERP系统的地址  | 
| 
 SIGN_KEY  | 
 是  | 
 验签密钥  | 
 客户在ERP系统配置内容  | 
| 
 CLIENT_ID  | 
 是  | 
 企业唯一标识  | 
 客户在ERP系统配置内容  | 
| 
 EXPIRE_TIME  | 
 否  | 
 jwt-token过期时间(默认3600秒)  | 
 客户自定义  | 
| 
 uniqueKey  | 
 是  | 
 客户企业用户唯一标识(不可变)  | 
 客户系统内部用户数据  | 
| 
 mobile  | 
 否  | 
 用户手机号  | 
 客户系统内部用户数据  | 
| 
 | 
 否  | 
 用户邮箱  | 
 客户系统内部用户数据  | 
| 
 username  | 
 否  | 
 用户名  | 
 客户系统内部用户数据  | 
| 
 realname  | 
 否  | 
 真实姓名  | 
 客户系统内部用户数据  | 
注意事项:
1、使用企业用户唯一标识字段(uniqueKey):建议设置为您系统的唯一且不可变的字段,例如用户ID。用户初次映射成功后,后续变更了用户基本信息(手机号/邮箱/用户名),依旧会通过获取初次映射的uniqueKey对应的用户数据进行单点登录。
2、客户基本信息(mobile/email/username): 当企业JWT配置中设置手机号为用户唯一性映射字段时,手机号便不允许为空,其余字段同理。
3.2 基于领星SDK实现
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("领星ERP用户")
                .build();
        String redirectUrl = null;
        try {
            redirectUrl = client.getRedirectUrl(loginDTO);
        } catch (SSOLoginException e) {
            // TODO 异常处理
        }
        System.out.println(redirectUrl);
    }
}
3. 客户需要替换的变量
| 
 变量名称  | 
 是否必填  | 
 变量说明  | 
 值来源  | 
| 
 domain  | 
 是  | 
 访问ERP系统的域名 (例如: http://erp.lingxing.com)  | 
 客户访问ERP系统的地址  | 
| 
 signKey  | 
 是  | 
 验签密钥  | 
 客户在ERP系统配置内容  | 
| 
 clientId  | 
 是  | 
 企业唯一标识  | 
 客户在ERP系统配置内容  | 
| 
 expireMilliseconds  | 
 否  | 
 jwt-token过期时间(默认3600秒)  | 
 客户自定义  | 
| 
 uniqueKey  | 
 是  | 
 客户企业用户唯一标识(不可变)  | 
 客户系统内部用户数据  | 
| 
 mobile  | 
 否  | 
 用户手机号  | 
 客户系统内部用户数据  | 
| 
 | 
 否  | 
 用户邮箱  | 
 客户系统内部用户数据  | 
| 
 username  | 
 否  | 
 用户名  | 
 客户系统内部用户数据  | 
| 
 realname  | 
 否  | 
 真实姓名  | 
 客户系统内部用户数据  | 
4、单点登录领星ERP系统
打开新的浏览器页签,单点登录领星ERP系统(需要更换为自己的域名,以下为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
未能解决你的问题?请联系在线客服
请问有什么疑问?
请问有什么疑问?