某不知名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×tamp=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));
}
}