diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/WxLoginController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/WxLoginController.java new file mode 100644 index 0000000..29fe6b1 --- /dev/null +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/WxLoginController.java @@ -0,0 +1,94 @@ +package com.agileboot.api.controller; + +import com.agileboot.common.core.dto.ResponseDTO; +import com.agileboot.common.exception.ApiException; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.agileboot.domain.ab98.api.Ab98ApiUtil; + +import javax.validation.constraints.NotBlank; +import java.util.Map; + +@RestController +@RequestMapping("/api/wx/login") +@CrossOrigin(origins = "*", allowedHeaders = "*") +@RequiredArgsConstructor +@Api(tags = "微信登录接口") +public class WxLoginController { + + @PostMapping("/logout") + @ApiOperation(value = "用户退出登录") + public ResponseDTO logout(@RequestParam @NotBlank String token) { + try { + return ResponseDTO.ok(Ab98ApiUtil.doLogout(token)); + } catch (ApiException e) { + return ResponseDTO.fail(e); + } + } + + /** + * 获取微信登录二维码链接 + */ + @GetMapping("/wechat/qrcode") + public ResponseDTO getWechatQrCode(@RequestParam @NotBlank String token) { + try { + return ResponseDTO.ok(Ab98ApiUtil.generateWechatLoginUrl(token)); + } catch (ApiException e) { + return ResponseDTO.fail(e); + } + } + + + /** + * 获取临时token + */ + @GetMapping("/getToken") + @ApiOperation(value = "获取临时令牌", notes = "用于后续登录流程") + public ResponseDTO getToken(@RequestParam String appName) { + try { + return ResponseDTO.ok(Ab98ApiUtil.getToken(appName)); + } catch (ApiException e) { + return ResponseDTO.fail(e); + } + } + + /** + * 发送短信验证码 + */ + @PostMapping("/sendSms") + public ResponseDTO sendSms( + @RequestParam String token, + @RequestParam String tel) { + try { + return ResponseDTO.ok(Ab98ApiUtil.sendLoginSms(token, tel)); + } catch (ApiException e) { + return ResponseDTO.fail(e); + } + } + + /** + * 验证短信验证码 + */ + @PostMapping("/verifySms") + public ResponseDTO verifySms( + @RequestParam String token, + @RequestParam String tel, + @RequestParam String vcode) { + try { + Ab98ApiUtil.LoginResponse loginResponse = Ab98ApiUtil.verifySmsCode(token, tel, vcode); + Ab98ApiUtil.LoginData data = new Ab98ApiUtil.LoginData(); + data.setFace_img(loginResponse.getOutputData().getFace_img()); + data.setSuccess(loginResponse.getOutputData().isSuccess()); + data.setSex(loginResponse.getOutputData().getSex()); + data.setName(loginResponse.getOutputData().getName()); + data.setUserid(loginResponse.getOutputData().getUserid()); + data.setRegistered(loginResponse.getOutputData().isRegistered()); + data.setTel(loginResponse.getOutputData().getTel()); + return ResponseDTO.ok(data); + } catch (ApiException e) { + return ResponseDTO.fail(e); + } + } +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/ab98/api/Ab98ApiUtil.java b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/api/Ab98ApiUtil.java new file mode 100644 index 0000000..36d8c93 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/ab98/api/Ab98ApiUtil.java @@ -0,0 +1,234 @@ +package com.agileboot.domain.ab98.api; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.agileboot.common.exception.ApiException; +import com.agileboot.common.exception.error.ErrorCode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +public class Ab98ApiUtil { + + private static final String BASE_URL = "https://www.ab98.cn/api/doInterface"; + private static final String WEBSOCKET_URL = "wss://www.ab98.cn/login.ws/"; + + /** + * 短信登录(发送验证码) + */ + /** + * 发送短信验证码(短信登录) + * @param token 通过getToken获取的临时令牌(有效期5分钟) + * @param tel 接收验证码的手机号码(需符合格式:11位数字) + * @param nobind "true"表示不绑定手机号(固定值) + * @param for_login "true"表示用于登录(固定值) + * @param from 来源渠道(固定值"jt") + */ + public static SmsSendResponse sendLoginSms(String token, String tel) { + String url = BASE_URL + "?code=doSendSms"; + + Map paramMap = new HashMap() {{ + put("token", token); + put("tel", tel); + put("nobind", "true"); + put("for_login", "true"); + put("from", "jt"); + }}; + + String response = HttpUtil.createPost(url) + .body(JSONUtil.toJsonStr(paramMap)) + .header("noSign", "true") + .header("source", "api") + .execute().body(); + log.info("短信发送响应: {}", response); + + SmsSendResponse resp = JSONUtil.toBean(response, SmsSendResponse.class); + checkApiResponse(resp); + return resp; + } + + /** + * 验证短信验证码 + */ + /** + * 验证短信验证码(短信登录) + * @param token 通过getToken获取的临时令牌 + * @param tel 接收验证码的手机号码 + * @param vcode 用户输入的6位验证码(有效期5分钟) + * @return 登录结果,包含用户身份信息 + * @throws ApiException 当验证失败或接口返回异常时抛出 + */ + public static LoginResponse verifySmsCode(String token, String tel, String vcode) { + String url = BASE_URL + "?code=doCheckSmsCode"; + + Map paramMap = new HashMap() {{ + put("token", token); + put("tel", tel); + put("vcode", vcode); + }}; + + String response = HttpUtil.createPost(url) + .body(JSONUtil.toJsonStr(paramMap)) + .header("noSign", "true") + .header("source", "api") + .execute().body(); + log.info("短信验证响应: {}", response); + + LoginResponse resp = JSONUtil.toBean(response, LoginResponse.class); + checkApiResponse(resp); + return handleLoginResult(resp); + } + + /** + * 处理登录结果 + */ + private static LoginResponse handleLoginResult(LoginResponse response) { + if (response.getOutputData() == null || !response.getOutputData().isSuccess()) { + log.error("登录失败: {}", response); + throw new ApiException(ErrorCode.FAILED, "登录验证失败"); + } + return response; + } + + /** + * 生成微信扫码登录URL + */ + public static String generateWechatLoginUrl(String token) { + return "https://www.ab98.cn/online/index.html?content=doLogin%60" + token; + } + + /** + * 监听扫码登录websocket + */ + /*public static void listenLoginWebsocket(String token, WebSocketListener listener) { + String url = WEBSOCKET_URL + token; + HttpUtil.createWebSocket(url, listener).connect(); + }*/ + + /** + * 获取登录token + */ + /** + * 获取登录token + * @param appName 应用英文名(需在汇邦数字平台登记) + * @return outputData.token 临时令牌(后续接口需携带) + * @return outputData.takeFace 是否需要人脸验证(true需要刷脸) + */ + public static TokenResponse getToken(String appName) { +// String url = BASE_URL + "?code=doGetToken&from=jt&app=" + appName; + String url = BASE_URL + "?code=doGetToken&from=jt"; + + String response = HttpUtil.createGet(url) + .header("noSign", "true") + .header("source", "api") + .execute().body(); + log.info("获取token响应: {}", response); + + TokenResponse tokenResponse = JSONUtil.toBean(response, TokenResponse.class); + checkApiResponse(tokenResponse); + return tokenResponse; + } + + /** + * 构建带参数的请求URL + */ + + + /** + * 检查接口响应状态 + */ + private static void checkApiResponse(BaseResponse response) { + if (response.getStateCode() != 200 || !"ok".equals(response.getState())) { + log.error("接口调用失败: {}", response); + throw new ApiException(ErrorCode.FAILED, "第三方接口调用失败"); + } + } + + // 基础响应对象 + @Data + public static class BaseResponse { + private String state; + private Integer stateCode; + private Object outputData; + } + + @Data + public static class SmsSendResponse extends BaseResponse { + private SmsResult outputData; + } + + @Data + public static class SmsResult { + private boolean success; + private String errMsg; + } + + @Data + public static class LoginResponse extends BaseResponse { + private LoginData outputData; + } + + @Data + public static class LoginData { + private String idcard_back; // 身份证背面照片地址 + private String face_img; // 人脸照片地址 + private String address; // 身份证登记地址 + private String nation; // 民族(接口文档显示可能为null) + private boolean success; // 验证是否成功 + private String sex; // 性别(男/女) + private String name; // 真实姓名 + private String userid; // 用户在平台的唯一ID + private boolean registered; // 是否已注册 + private String tel; // 手机号码 + private String idnum; // 身份证号码 + private String idcard_front; // 身份证正面照片地址 + } + + + /** + * 用户退出接口 + */ + public static LogoutResponse doLogout(String token) { + String url = BASE_URL + "?code=doLogout"; + + Map paramMap = new HashMap() {{ + put("token", token); + }}; + + String response = HttpUtil.createPost(url) + .body(JSONUtil.toJsonStr(paramMap)) + .header("noSign", "true") + .header("source", "api") + .execute().body(); + log.info("用户退出响应: {}", response); + + LogoutResponse resp = JSONUtil.toBean(response, LogoutResponse.class); + checkApiResponse(resp); + return resp; + } + + @Data + public static class LogoutResponse extends BaseResponse { + private LogoutData outputData; // 包含success和logoutTime(登出时间) + } + + @Data + public static class LogoutData { + private boolean success; + private String logoutTime; + } + + // Token响应对象 + @Data + public static class TokenResponse extends BaseResponse { + private TokenData outputData; + } + + @Data + public static class TokenData { + private String token; + private boolean takeFace; + } +} \ No newline at end of file diff --git a/doc/汇邦数字身份平台登录接口文档.md b/doc/汇邦数字身份平台登录接口文档.md new file mode 100644 index 0000000..2b41270 --- /dev/null +++ b/doc/汇邦数字身份平台登录接口文档.md @@ -0,0 +1,232 @@ +## 请求概述 +### 请求header +所有请求必须包含以下两个header: +* `noSign`: `true` +* `source`: `api` +获取登录token后,也可以在header中添加: +* `token`: `${token}` +### 登录校验结果 +接口返回结果为json格式,如果包含`code`字段,则说明登录校验失败。 +* `code`: `0001` - 登录状态已失效 +* `code`: `0002` - 在cookie、Get参数、Header中均未检测到token +## 登录接口 +### 2.1 获取token +* **API**: `https://www.ab98.cn/api/doInterface?code=doGetToken&from=jt&app=${appname}` +* **请求方式**: GET +* **请求参数**: + * `code`: `doGetToken` (必传) + * `from`: `jt` (可选) + * `app`: 调用该接口的应用在汇邦数字平台登记的应用英文名 (可选) +**成功返回**: +```json +{ + "outputData": { + "token": "1a6ea39e84c406283839856640e3aa66", + "takeFace": true + }, + "state": "ok" +} +``` +**失败返回**: +```json +{ + "outputData": "错误信息", + "stateCode": 0, + "state": "fail" +} +``` +### 2.2 短信登录 +#### 2.2.1 发送短信 +* **API**: `https://www.ab98.cn/api/doInterface?code=doSendSms` +* **请求方式**: POST +* **Content-Type**: application/json +* **请求BODY**: +```json +{ + "token":"358900e1005c33a1dd059b07042ceec3", //必传 + "tel":"137xxxxxxxx", //必传 + "nobind":"true", //必传 + "for_login":"true", //必传 + "from":"jt" //必传 +} +``` +**成功返回**: +```json +{ + "outputData": { + "success": true + }, + "state": "ok" +} +``` +**失败返回**: +```json +{ + "outputData": { + "success": false, + "errMsg": "失败原因" + }, + "state": "ok" +} +``` +#### 2.2.2 验证短信验证码 +* **API**: `https://www.ab98.cn/api/doInterface?code=doCheckSmsCode` +* **请求方式**: POST +* **Content-Type**: application/json +* **请求BODY**: +```json +{ + "token":"358900e1005c33a1dd059b07042ceec3", + "tel":"137xxxxxxxx", + "vcode":"123456" +} +``` +**成功返回**: +```json +{ + "outputData": { + "idcard_back": "https://www.ab98.cn/upload/temp/images/202102/xxx.jpeg", + "face_img": "https://www.ab98.cn/upload/temp/images/202102/yyy.jpeg", + "address": "身份证上的地址", + "nation": null, + "success": true, + "sex": "男", + "name": "xxx", + "userid": 123, + "registered": true, + "tel": "137xxxxxxxx", + "idnum": "450981xxxxxxxxxxxx", + "idcard_front": "https://www.ab98.cn/upload/temp/images/202102/zzz.jpeg" + }, + "state": "ok" +} +``` +**失败返回**: +```json +{ + "outputData": { + "success": false + }, + "state": "ok" +} +``` +### 2.3 微信扫码登录 +* **API**: `https://www.ab98.cn/online/index.html?content=doLogin%60` +#### 2.3.1 二维码 +假设获取到的token为`358900e1005c33a1dd059b07042ceec3`,则构造的最终链接为: +`https://www.ab98.cn/online/index.html?content=doLogin%60358900e1005c33a1dd059b07042ceec3` +使用该链接生成二维码图片供用户使用微信“扫一扫”。 +#### 2.3.2 监听websocket +* **监听地址**: `wss://www.ab98.cn/login.ws/${token}` +* **登录失败**: +```json +{ +"outputData": "登录失败原因", +"state": "fail" +} +``` +* **登录成功**: +```json +{ +"outputData": "登录成功", +"state": "ok", +"username": "扫码者姓名", +"sex": "扫码者性别", +"head_img": "扫码者头像", +"idnum": "身份证号码", +"userid": "人员记录ID", +"tel": "手机号码" +} +``` +### 2.4 刷脸登录 +* **API**: `https://www.ab98.cn/api/doInterface?code=doStrongFaceLogin` +* **请求方式**: POST +* **Content-Type**: application/json +* **请求BODY**: +```json +{ +"token":"358900e1005c33a1dd059b07042ceec3", //必传 +"check_code":"1234", //必传 +"imgBase64":"/9j/4AAQS…" //必传 +} +``` +**成功返回**: +```json +{ +"state": "ok", +"outputData": "登录成功", +"openid": "oGfFD1jBEZ2sq4PhOc8zKKejHA9E", +"head_img": "https://www.ab98.cn/… ", +"sex": "男", +"userid": 1, +"token": "dbe11ef64cd6350c5d935d531db765c0", +"username": "xxx", +"tel": "xxx" +} +``` +### 2.5 动态码登录 +1. **发起websocket监听**: +`wss://www.ab98.cn/login.ws/{监听token}` +其中 `{监听token}` 由客户端自定义,由数字和英文字母组成,保证多用户同时监听时 `{监听token}` 互不相同即可,否则会接收到错误消息。 +2. **主动发送消息**: +```json +{ +"type": "mfa", +"do": "checkCode", +"token": "xxx", +"code": "000000" +} +``` +**服务器响应消息**: +1. **登录失败**: +```json +{ +"state": "fail", +"msg": "失败原因" +} +``` +2. **动态码校验通过,等待移动端授权登录**: +```json +{ +"state": "ok", +"step": "0" +} +``` +3. **等待移动端授权超时**: +```json +{ +"state": "ok", +"step": "1" +} +``` +4. **移动端已授权登录**: +```json +{ +"state": "ok", +"step": "2", +"openid": "xxx", +"head_img": "/xxx", +"sex": "男/女", +"company": "xxx", +"userid": 123, +"idnum": "xxx", +"username": "xxx", +"tel": "xxx" +} +``` +### 2.6 退出登录 +* **API**: `https://www.ab98.cn/api/doInterface?code=doLogout` +* **请求方式**: POST +* **Content-Type**: application/json +* **请求BODY**: +```json +{ +"token":"358900e1005c33a1dd059b07042ceec3" +} +``` +**成功返回**: +```json +{ +"state": "ok" +} +``` \ No newline at end of file