单点登录(连登方案)

某不知名app联合登录联调说明文档

1.跳转第三方授权流程

某不知名服务已打码
在这里插入图片描述

2. 账号申请

向app方提出申请,由app方提供AK及SK值

3.获取authCode

接口名称获取authCode
接口描述适用于登录某不知名app后,跳转第三方时,通过该接口获取某不知名生成某不知名的token时所必须的authCode信息使用该authCode信息,可调用某不知名服务获取某不知名token信息该authCode信息不可以重复使用,失效周期:未操作的情况下3分钟失效,使用后该authCode会立即失效
接口地址/user/jointLogin/getAuthCode
http调用方法Get

3.1请求参数

在这里插入图片描述

3.2返回参数

在这里插入图片描述

{
    "code":"200",
    "message":"success",
    "data":"1371edoi8uwhqyhqj449sadn3kjbhsd712kd213df25yb6fdfr23wew"
}

4.根据authCode获取用户三方token信息

在这里插入图片描述

4.1请求参数

在这里插入图片描述
请求示例

{
    "authCode":"9sadn3kjbhsd712kd213df25yb6fdfr23wew"
}

4.2返回参数

在这里插入图片描述
返回示例

{
    "code":"200",
    "message":"成功",
    "data":" eyJUeXBlIjoiSnd0IiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJ0b2tlblR5cGUiOiJhZG1pbl90b2tlbiIsInRva2VuIjoie1wiaWRcIjoxMDAxMDQ4LFwibmFtZVwiOlwiY29weXlod2MxMFwiLFwicm9sZXNcIjpcIui2heeuoSzotoXnrqEs6LaF566hLOi2heeuoSzotoXnrqEs6LaF566hLOi2heeuoSzotoXnrqEs6LaF566hLOi2heeuoSzotoXnrqFcIixcInRva2VuVHlwZVwiOlwiYWRtaW5fdG9rZW5cIn0ifQ.4HH01QtTckhUufOO9vX8ikgkMexMM3rqKa3WWU13jEA "
}

5根据token调用某不知名api获取用户信息

在这里插入图片描述

5.1请求参数

在这里插入图片描述

5.2返回参数

在这里插入图片描述
返回示例

{
    "code":"200",
    "message":"成功",
    "data":{
        "user":{
            "phone":"150****0000",
            "personId":"1yhy36yq6et35qy8iuqhw8iqwjq452e"
		}
	}
}

6.加签规则

6.1加签字段

在这里插入图片描述

6.2加签方式

1、拼接参数(xxx为参数值)
参数名和值使用(=)符号连接,参数之前依照顺序使用(&)符号连接
access_key=xxx&timestamp=xxx&authCode=xxx
2、加密
使用某不知名提供的SK对拼接后的字符串使用SM2加密方式进行公钥加密按照BCD编码规则编码成字符串
3、将编码后的字符串通过header参数signature参数传输

7.联调说明

1、进入跳转页面后需要第一时间调用"获取authCode",再由返回的code作为入参调用"根据authCode获取用户三方token信息"接口获取token信息,后续业务接口需要依赖次token信息
2、"根据authCode获取用户三方token信息"返回的token信息可以重复使用,三方需对此token信息作映射存储,以供后续其他业务接口调用。


**代码部分 **

1.controller


    @Autowired
    JointLoginInterface jointLoginInterface;
    
    @Autowired
    JointLoginConfig jointLoginConfig;
    
    @GetMapping("/getAuthCode")
    @CheckLogin
    public HttpMessageResult<String> getAuthCode() {
        final String bizCode = "getAuthCode";
        LoggerUtil.info(LOGGER, "[{0}] start", bizCode);
        return BizHandlerTemplate.execute(new HandlerCallBack<String>() {
            @Override
            public void paramCheck() throws Exception {
            }

            @Override
            public String process() throws Exception {
                return jointLoginInterface.getAuthCode(ThreadLocalUtils.getLoginUser());
            }
        }, bizCode);
    }


 /**
     * 通过authCode获取用户信息
     * 第三方调用
     *
     * @param request 临时token
     * @return 用户信息
     */
    @PostMapping("/getPersonInfoByAuthCode")
    public HttpMessageResult<JointLoginPersonResponse> getPersonInfoByAuthCode(HttpServletRequest request, @RequestBody JointLoginRequest requestBody) {
        final String bizCode = "getPersonInfoByAuthCode";
        LoggerUtil.info(LOGGER, "[{0}] start param={1}", bizCode, requestBody);
        return BizHandlerTemplate.execute(new HandlerCallBack<JointLoginPersonResponse>() {
            //获取请求中header的参数
            String accessKey = request.getHeader("access_key");
            String timestamp = request.getHeader("timestamp");
            String signature = request.getHeader("signature");
            String requestId = request.getHeader("request_id");

            @Override
            public void paramCheck() throws Exception {
                if (ObjectUtils.isEmpty(accessKey)) {
                    LoggerUtil.warn(LOGGER, "accessKey信息为空");
                    throw new BizException(ResponseStatusEnum.PARAM_WRONG);
                }
                if (ObjectUtils.isEmpty(timestamp)) {
                    LoggerUtil.warn(LOGGER, "timestamp信息为空");
                    throw new BizException(ResponseStatusEnum.PARAM_WRONG);
                }
                if (ObjectUtils.isEmpty(signature)) {
                    LoggerUtil.warn(LOGGER, "signature签名信息为空");
                    throw new BizException(ResponseStatusEnum.PARAM_WRONG);
                }
                if (ObjectUtils.isEmpty(requestId)) {
                    LoggerUtil.warn(LOGGER, "requestId信息为空");
                    throw new BizException(ResponseStatusEnum.PARAM_WRONG);
                }
                if (ObjectUtils.isEmpty(requestBody) || ObjectUtils.isEmpty(requestBody.getAuthCode())) {
                    LoggerUtil.warn(LOGGER, "authCode信息为空");
                    throw new BizException(ResponseStatusEnum.JOINT_LOGIN_AUTH_CODE_IS_NULL);
                }

            }

            @Override
            public void businessCheck() throws Exception {
                LoggerUtil.info(LOGGER, "第三方获取用户信息,requestId={0},accessKey={1},timestamp={2},signature={3}", requestId, accessKey, timestamp, signature);
                //校验accessKey是否有效
                JointLoginAccountProperties akConfig = checkAccessKey(accessKey);
                //校验signature签名
                if (!checkSign(akConfig, accessKey, timestamp, requestBody.getAuthCode(), signature)) {
                    throw new BizException(ResponseStatusEnum.REQUEST_ILLEGAL);
                }
            }

            @Override
            public JointLoginPersonResponse process() throws Exception {
                return jointLoginInterface.getPersonInfoByAuthCode(requestBody.getAuthCode());
            }
        }, bizCode);
    }

    /**
     * 通过token获取用户信息
     * 第三方调用
     *
     * @param request 临时token
     * @return 用户信息
     */
    @PostMapping("/getPersonInfo")
    public HttpMessageResult<SoilJointLoginPersonResponse> getPersonInfo(HttpServletRequest request) {
        final String bizCode = "getPersonInfo";
        LoggerUtil.info(LOGGER, "[{0}] start", bizCode);
        return BizHandlerTemplate.execute(new HandlerCallBack<SoilJointLoginPersonResponse>() {
            //获取请求中header的参数
            String token = request.getHeader("Authorization");

            @Override
            public void paramCheck() throws Exception {
                if (StringUtils.isBlank(token)) {
                    LoggerUtil.warn(LOGGER, "token信息为空");
                    throw new BizException(ResponseStatusEnum.USER_TOKEN_VALID);
                }
            }

            @Override
            public void businessCheck() throws Exception {

            }

            @Override
            public SoilJointLoginPersonResponse process() throws Exception {
                return jointLoginInterface.getPersonInfo(token);
            }
        }, bizCode);
    }

    /**
     * 校验第三方AK
     *
     * @param accessKey 第三方传输从AK
     * @return 第三方的密钥信息
     */
    private JointLoginAccountProperties checkAccessKey(String accessKey) {
        List<JointLoginAccountProperties> list = jointLoginConfig.getPlatform();
        for (JointLoginAccountProperties platform : list) {
            if (accessKey.equals(platform.getAccessKey())) {
                return platform;
            }
        }
        LoggerUtil.warn(LOGGER, "accessKey无效 ak={0}", accessKey);
        throw new BizException(ResponseStatusEnum.JOINT_LOGIN_AK_SK_ERROR);
    }


    /**
     * 校验签名规则
     *
     * @param akConfig  配置的ak信息
     * @param accessKey 第三方传递的ak信息
     * @param timestamp 第三方传递的时间戳
     * @param authCode  第三方传递的authCode
     * @param signature 第三方传递的签名
     * @return 是否校验成功,校验成功true, 校验失败false
     */
    private boolean checkSign(JointLoginAccountProperties akConfig, String accessKey, String timestamp, String authCode, String signature) {
        String signStr;
        try {
            signStr = Sm2Util.decrypt(akConfig.getPrivateKey(), signature);
        } catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "签名解密失败,签名={0}", signature);
            return false;
        }
        LoggerUtil.debug(LOGGER, "解密后签名串={0};接收到的参数签名规则串={1}", signStr, signature);
        String str = "access_key=" + accessKey + "&" +
                "timestamp=" + timestamp + "&" +
                "authCode=" + authCode;
        return signStr.equals(str);
    }

2.impl实现类

   @Service
public class JointLoginManager implements JointLoginInterface {

    private static final Logger LOGGER = LoggerFactory.getLogger(JointLoginManager.class);

    private static final Long TIME = 7200L;

    @Autowired
    RedisUtil redisUtil;
    @Autowired
    JointLoginConfig jointLoginConfig;
    @Autowired
    private YhwcLoginMapper loginMapper;
    @Autowired
    TripartiteDockingRecordMapper tripartiteDockingRecordMapper;

    @Override
    public String getAuthCode(Person person) {
        if (ObjectUtils.isEmpty(person)) {
            LoggerUtil.warn(LOGGER, "生成authCode时,用户信息为空");
            throw new BizException(ResponseStatusEnum.DATA_NOT_EXISTS);
        }
        //临时token规则:personId+yyyyMMdd HH:mm  然后再sm3哈希
        String token = person.getPersonId() + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd HH:mm"));
        //sm3哈希
        token = Sm3Util.encrypt(token);
        AuthCodeDTO authCodeDTO = new AuthCodeDTO();
        authCodeDTO.setAccount(person.getAccount());
        authCodeDTO.setPersonId(person.getPersonId());
        boolean flag = redisUtil.set(RedisKeySplitUtil.getAuthCodeKey(token), authCodeDTO, 180);
        LoggerUtil.info(LOGGER, "生成authCode结果personId={0},结果={0}", person.getPersonId(), flag);
        if (!flag) {
            throw new BizException(ResponseStatusEnum.JOINT_LOGIN_GET_AUTH_CODE_ERROR);
        }
        return token;
    }

    @Override
    public JointLoginPersonResponse getPersonInfoByAuthCode(String authCode) {
        String key = RedisKeySplitUtil.getAuthCodeKey(authCode);
        AuthCodeDTO authCodeDTO = redisUtil.getObj(key, AuthCodeDTO.class);
        if (ObjectUtils.isEmpty(authCodeDTO)) {
            throw new BizException(ResponseStatusEnum.JOINT_LOGIN_AUTH_CODE_NOT_FOUND);
        }
        redisUtil.del(key);
        JointLoginPersonResponse jointLoginPersonResponse = new JointLoginPersonResponse();
        jointLoginPersonResponse.setPhone(authCodeDTO.getAccount());
        return jointLoginPersonResponse;
    }

    /**
     * 通过authCode获取token信息
     *
     * @param authCode
     * @return
     */
    @Override
    public String getTokenByAuthCode(String authCode) {
        String key = RedisKeySplitUtil.getAuthCodeKey(authCode);
        AuthCodeDTO authCodeDTO = redisUtil.getObj(key, AuthCodeDTO.class);
        if (ObjectUtils.isEmpty(authCodeDTO) || StringUtils.isBlank(authCodeDTO.getAccount())) {
            throw new BizException(ResponseStatusEnum.JOINT_LOGIN_AUTH_CODE_NOT_FOUND);
        }
        redisUtil.del(key);
        LoginUserDTO loginUser = loginMapper.selectUserAccount(authCodeDTO.getAccount());
        UserTokenDTO userTokenDTO = UserTokenConvertUtil.convertUserTokenDTO(loginUser);
        String generateToken = TripartiteJwtUtils.generateToken(userTokenDTO, ChannelLoginEnum.SOIL_FLOW_NET_LOGIN_CHANNEL.getCode());
        //存入token至redis
        LoggerUtil.info(LOGGER, "loginUser={0}", loginUser);
        LoggerUtil.info(LOGGER, "generateToken={0}", generateToken);
        redisUtil.set(loginUser.getUserId() + "_" + ChannelLoginEnum.SOIL_FLOW_NET_LOGIN_CHANNEL.getCode(), generateToken, getExpirationTime());
        return generateToken;
    }

    private Long getExpirationTime() {
        List<JointLoginAccountProperties> list = jointLoginConfig.getPlatform();
        Long soilExpirationTime = list.get(0).getSoilExpirationTime();
        if (Objects.isNull(soilExpirationTime)) {
            soilExpirationTime = TIME;
        }
        LoggerUtil.info(LOGGER, "soilExpirationTime={0}", soilExpirationTime);
        return soilExpirationTime;
    }

    /**
     * 查询用户信息
     *
     * @param token
     * @return
     */
    @Override
    public SoilJointLoginPersonResponse getPersonInfo(String token) {
        UserTokenDTO tokenDTO = TripartiteJwtUtils.parseToken(token, ChannelLoginEnum.SOIL_FLOW_NET_LOGIN_CHANNEL.getCode());
        JointLoginPersonResponse jointLoginPersonResponse = new JointLoginPersonResponse();
        jointLoginPersonResponse.setPhone(AesUtils.encode(tokenDTO.getAccount()));
        return new SoilJointLoginPersonResponse().setUser(jointLoginPersonResponse);
    }

    /**
     * 三方连登获取authCode码
     *
     * @param person
     * @return
     */
    @Override
    public TripartiteLoginResponse getAuthorizationCode(Person person) {
        if (ObjectUtils.isEmpty(person)) {
            LoggerUtil.warn(LOGGER, "生成authCode时,用户信息为空");
            throw new BizException(ResponseStatusEnum.DATA_NOT_EXISTS);
        }
        if (StringUtils.isBlank(person.getPhone())) {
            LoggerUtil.warn(LOGGER, "生成authCode时,用户手机号码为空");
            throw new BizException(ResponseStatusEnum.DATA_NOT_EXISTS);
        }
        //临时token规则:personId+yyyyMMdd HH:mm  然后再sm3哈希
        String token = person.getPersonId() + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd HH:mm"));
        //sm3哈希
        token = Sm3Util.encrypt(token);
        AuthCodeDTO authCodeDTO = new AuthCodeDTO();
        authCodeDTO.setAccount(person.getAccount());
        authCodeDTO.setPersonId(person.getPersonId());
        boolean flag = redisUtil.set(RedisKeySplitUtil.getAuthCodeKey(token), authCodeDTO, 180);
        LoggerUtil.info(LOGGER, "生成authCode结果personId={0},结果={0}", person.getPersonId(), flag);
        if (!flag) {
            throw new BizException(ResponseStatusEnum.JOINT_LOGIN_GET_AUTH_CODE_ERROR);
        }
        TripartiteDockingRecordDO phoneInfo = tripartiteDockingRecordMapper.getPhoneInfo(person.getAccount());
        TripartiteLoginResponse response = new TripartiteLoginResponse();
        response.setAuthCode(token);
        response.setFirstLogin(Objects.isNull(phoneInfo));
        return response;
    }

    /**
     * 保存协议信息
     *
     * @return
     */
    @Override
    public Boolean saveLoginInfo() {
        if (Objects.isNull(ThreadLocalUtils.getLoginUser()) || StringUtils.isBlank(ThreadLocalUtils.getLoginUser().getPhone())) {
            LoggerUtil.warn(LOGGER, "同意协议时,用户信息为空");
            throw new BizException(ResponseStatusEnum.DATA_NOT_EXISTS);
        }
        //入库前先查询是否存在唯一索引冲突
        TripartiteDockingRecordDO phoneInfo = tripartiteDockingRecordMapper.getPhoneInfo(ThreadLocalUtils.getLoginUser().getPhone());
        if (Objects.nonNull(phoneInfo)) {
            LoggerUtil.warn(LOGGER, "同意协议时,用户信息已存在");
            return false;
        }
        TripartiteDockingRecordDO recordDO = new TripartiteDockingRecordDO();
        recordDO.setPhone(ThreadLocalUtils.getLoginUser().getPhone());
        recordDO.setChannelType(PersonChannelIdEnum.SOIL_FLOW_NET.getCode());
        recordDO.setCreatedBy(ThreadLocalUtils.getCurrentUser());
        recordDO.setModifiedBy(ThreadLocalUtils.getCurrentUser());
        int result = tripartiteDockingRecordMapper.saveLoginInfo(recordDO);
        return result != 0;
    }
}

其他实体及工具类

import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.digest.SM3;

/**
 * SM3加密工具类
 *
 * @author 
 * @version : Sm3Util.java, v 0.1 2023年02月28日 10:09  Exp $
 */
public class Sm3Util {
    /**
     * 加密加上盐值
     *
     * @param str  待加密的串
     * @param salt 盐值
     * @return 密文
     */
    public static String encryptWithSalt(String str, String salt) {
        SM3 sm3 = SmUtil.sm3WithSalt(salt.getBytes());
        return sm3.digestHex(str);
    }

    /**
     * 加密
     *
     * @param str 待加密的串
     * @return 密文
     */
    public static String encrypt(String str) {
        return SmUtil.sm3(str);
    }


}
@EqualsAndHashCode(callSuper = true)
@Data
public class AuthCodeDTO extends BaseString {

    /**
     * 登录手机号
     */
    private String account;

    /**
     * 用户id
     */
    private String personId;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class JointLoginPersonResponse extends BaseString {

    /**
     * 登录的手机号
     */
    private String phone;
}
@Slf4j
public class TripartiteJwtUtils {

    /**
     * 生成token,自定义过期时间 毫秒
     *
     * @param userTokenDTO 用户信息
     * @param channelType  渠道
     * @return
     */
    public static String generateToken(UserTokenDTO userTokenDTO, String channelType) {
        try {
            JwtConfig jwtConfig = SpringContextUtil.getBean(JwtConfig.class);
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getTripartiteTokenSecret());
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HS256");
            return JWT.create()
                    .withHeader(header)
                    .withClaim("token", JSONObject.toJSONString(userTokenDTO))
                    .withClaim("channelType", channelType)
                    .withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                    .sign(algorithm);
        } catch (Exception e) {
            log.error("generate token occur error, error is:{}", e);
            return null;
        }
    }

    /**
     * 检验token是否正确
     *
     * @param token
     * @return
     */
    public static UserTokenDTO parseToken(String token, String channelType) {
        if (StringUtils.isBlank(channelType)) {
            throw new SecurityException("渠道不能为空");
        }
        DecodedJWT jwt = null;
        JwtConfig jwtConfig = SpringContextUtil.getBean(JwtConfig.class);
        try {
            Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getTripartiteTokenSecret());
            JWTVerifier verifier = JWT.require(algorithm).build();
            jwt = verifier.verify(token);
        } catch (Throwable throwable) {
            throw new SecurityException("Token不合法");
        }
        if (Objects.isNull(jwt)) {
            throw new SecurityException("DecodedJWT解析失败");
        }
        String channel = jwt.getClaim("channelType").asString();
        if (!channelType.equals(channel)) {
            throw new SecurityException("渠道不合法");
        }
        String tokenInfo = jwt.getClaim("token").asString();
        return JSON.parseObject(tokenInfo, UserTokenDTO.class);
    }
}
@Data
public class JointLoginAccountProperties {

    /**
     * 用户id
     */
    private String accessKey;

    /**
     * 加密公钥
     */
    private String secretKey;

    /**
     * 加密私钥
     */
    private String privateKey;

    /**
     * 用户id
     */
    private String soilAccessKey;

    /**
     * 加密私钥
     */
    private String soilPrivateKey;

    /**
     * 过期时间
     */
    private Long soilExpirationTime;
}

nacos配置(自己配自己的,也可使用@Value拿配置)

@Data
@ConfigurationProperties(prefix = "app.joint")
@Component
public class JointLoginConfig {

    /**
     * 联登第三方平台aksk
     */
    private List<JointLoginAccountProperties> platform;

}
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;

import java.security.PublicKey;


public class Sm2Util {


    /**
     * 生成公私钥对(默认压缩公钥)
     */
    public static void genKeyPair() {
        //自动生成sm2密钥
        SM2 sm2 = SmUtil.sm2();
        //压缩私钥
        byte[] privateKey = BCUtil.encodeECPrivateKey(sm2.getPrivateKey());
        //压缩公钥
        byte[] publicKey = BCUtil.encodeECPublicKey(sm2.getPublicKey());
        //转为16进制
        String priKey = HexUtil.encodeHexStr(privateKey);
        String pubKey = HexUtil.encodeHexStr(publicKey);
        System.out.println("私钥|" + priKey);
        System.out.println("公钥|" + pubKey);
    }

    /**
     * SM2公钥加密
     *
     * @param publicKey 公钥
     * @param text      数据
     * @return 密文
     */
    public static String encrypt(String publicKey, String text) {
        PublicKey p = BCUtil.decodeECPoint(publicKey, SmUtil.SM2_CURVE_NAME);
        ECPublicKeyParameters ecPublicKeyParameters = BCUtil.toParams(p);
        //创建sm2 对象 公钥
        SM2 sm2 = new SM2(null, ecPublicKeyParameters);
        // 公钥加密
        return sm2.encryptBcd(text, KeyType.PublicKey);
    }


    /**
     * SM2私钥解密算法
     *
     * @param privateKey 私钥
     * @param cipherData 密文数据
     * @return 明文
     */
    public static String decrypt(String privateKey, String cipherData) {
        ECPrivateKeyParameters ecPrivateKeyParameters = BCUtil.toSm2Params(privateKey);
        //创建sm2 对象 私钥
        SM2 sm2 = new SM2(ecPrivateKeyParameters, null);
        // 私钥解密
        return StrUtil.utf8Str(sm2.decryptFromBcd(cipherData, KeyType.PrivateKey));
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值