一、概述
本指南旨在为“浙里办”单点登录组件提供接入指南,“浙里办”单点登陆组件,上架在IRS,为上架在IRS的应用,提供统一的单点登录解决方案,现阶段仅支持微信端的接入。
二、服务创建
3、应用接入“浙里办”单点登录组件前,需要先获取AK&SK。
三、接入说明
四、接入规范
- 接入“浙里办”微信小程序的 H5 应用(以下简称应用), 应当符合“同源发布”及无障碍适老化等要求。
- 应用应当在浙江省一体化数字资源系统(以下简称 IRS)发布,并使用统一的域名 https://mapi.zjzwfw.gov.cn/,作为接入微信小程序的前置条件。应用上架 IRS,应遵循 IRS 相关规范。
- 应用接入“浙里办”微信小程序,应当按照本指南操作步骤与注意事项,进行微信端的兼容适配。
五、操作步骤
“浙里办”微信小程序接入工作流程图:
1、单点登录适配
Java代码案例:
(1)Constants 定义所有常量
/**
* @author jie.chen
* @date 2022-03-30 15:24
*/
public interface Constants {
/**
* 单点登录 ticketId换token的地址
*/
// String ACCESS_TOKEN_URL = "https://bcdsg.zj.gov.cn:8443/restapi/prod/IC33000020220329000007/uc/sso/access_token";政务外网
//互联网
String ACCESS_TOKEN_URL = "https://ibcdsg.zj.gov.cn:8443/restapi/prod/IC33000020220329000007/uc/sso/access_token";
/**
* 单点登录 token获取用户信息地址
*/
// String GET_USER_INFO_URL = "https://bcdsg.zj.gov.cn:8443/restapi/prod/IC33000020220329000008/uc/sso/getUserInfo";政务外网
//互联网
String GET_USER_INFO_URL = "https://ibcdsg.zj.gov.cn:8443/restapi/prod/IC33000020220329000008/uc/sso/getUserInfo";
/**
* IRS请求携带的请求头
*/
String X_BG_HMAC_ACCESS_KEY = "X-BG-HMAC-ACCESS-KEY";
String X_BG_HMAC_SIGNATURE = "X-BG-HMAC-SIGNATURE";
String X_BG_HMAC_ALGORITHM = "X-BG-HMAC-ALGORITHM";
String X_BG_DATE_TIME = "X-BG-DATE-TIME";
/**
* IRS签名算法
*/
String DEFAULT_HMAC_SIGNATURE = "hmac-sha256";
/**
* 应用ID
*/
String APP_ID = "20******33";
/**
* 微信端固定值为weixin
*/
String WEIXIN_ENDPOINT_TYPE = "weixin";
/**
* IRS 申请组件生成的AK
*/
String IRS_AK = "********************************";
/**
* IRS 申请组件生成的SK
*/
String IRS_SK = "********************************";
String TOKEN_SESSION_KEY = "sessionAccessToken";
String USER_INFO_KEY = "sessionUserInfo";
}
(2)IrsUtils
/**
* @author jie.chen
* @date 2022-03-30 15:28
*/
public class IrsUtils {
@SneakyThrows
public static IrsSignRes sign(String url, String method) {
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(url).build();
uriComponents = uriComponents.encode();
List<String> queryArr = new ArrayList<>();
MultiValueMap<String, String> queryParams = uriComponents.getQueryParams();
for (Map.Entry<String, List<String>> next : queryParams.entrySet()) {
for (String va : next.getValue()) {
if (va == null) {
queryArr.add(next.getKey() + "=");
} else {
queryArr.add(next.getKey() + "=" + va);
}
}
}
//按照字典排序
Collections.sort(queryArr);
///Tue, 09 Nov 2021 08:49:20 GMT
DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
String dateTime = dateFormat.format(new Date());
String signStr = method.toUpperCase() + "\n" +
//拼接url path
uriComponents.getPath() + "\n" +
//拼接url query
String.join("&", queryArr) + "\n" +
Constants.IRS_AK + "\n" +
dateTime + "\n";
String sign = hmacSha256Base64(signStr, Constants.IRS_SK);
IrsSignRes res = new IrsSignRes();
res.setSignature(sign);
res.setAccessKey(Constants.IRS_AK);
res.setDateTime(dateTime);
res.setAlgorithm(Constants.DEFAULT_HMAC_SIGNATURE);
return res;
}
@SneakyThrows
private static String hmacSha256Base64(String content, String key) {
Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSHA256.init(secretKey);
byte[] bytes = hmacSHA256.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(bytes);
}
public static void main(String[] args) {
System.out.println(sign("https://bcdsg.zj.gov.cn:8443/restapi/prod/IC33000020220329000007/uc/sso/getUserInfo", "POST"));
}
}
(3)IrsSignRes
/**
* @author jie.chen
* @date 2022-03-30 15:28
*/
@Data
public class IrsSignRes {
private String accessKey;
private String signature;
private String algorithm;
private String dateTime;
}
(4)AuthService 业务实现类
/**
* @author jie.chen
* @date 2022-03-30 15:49
*/
@Component
public class AuthService {
@Autowired
private RestTemplateBuilder restTemplateBuilder;
private RestTemplate restTemplate;
@PostConstruct
void init() {
restTemplate = restTemplateBuilder.build();
}
public String getTokenByTicketId(String ticketId) {
HttpHeaders headers = getHttpHeaders(Constants.ACCESS_TOKEN_URL);
JSONObject body = new JSONObject();
body.put("appId", Constants.APP_ID);
body.put("ticketId", ticketId);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(Constants.ACCESS_TOKEN_URL, request, String.class);
return checkResponse(stringResponseEntity).getJSONObject("data").getString("accessToken");
}
public JSONObject getUserInfoByToken(String accessToken) {
HttpHeaders headers = getHttpHeaders(Constants.GET_USER_INFO_URL);
JSONObject body = new JSONObject();
body.put("token", accessToken);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(Constants.GET_USER_INFO_URL, request, String.class);
return checkResponse(stringResponseEntity).getJSONObject("data");
}
private JSONObject checkResponse(ResponseEntity<String> stringResponseEntity) {
if (!stringResponseEntity.getStatusCode().is2xxSuccessful()) {
//请求失败
throw new RuntimeException("status:" + stringResponseEntity.getStatusCodeValue() + " " + stringResponseEntity.getBody());
}
JSONObject result = JSON.parseObject(stringResponseEntity.getBody());
if (result.containsKey("errorCode") && result.getString("errorCode") != null && !result.getBooleanValue("success")) {
//业务错误
throw new RuntimeException(result.toString());
}
return result;
}
private HttpHeaders getHttpHeaders(String url) {
IrsSignRes res = IrsUtils.sign(url, "POST");
HttpHeaders headers = new HttpHeaders();
headers.add(Constants.X_BG_HMAC_ACCESS_KEY, res.getAccessKey());
headers.add(Constants.X_BG_HMAC_ALGORITHM, res.getAlgorithm());
headers.add(Constants.X_BG_HMAC_SIGNATURE, res.getSignature());
headers.add(Constants.X_BG_DATE_TIME, res.getDateTime());
return headers;
}
(5)LoginController 接口测试
/**
* @author hejun
* @since 2022-02-22 10:46:11
*/
@RestController
@RequestMapping("/user")
@Api(tags="用户登录")
@Slf4j
public class LoginController extends ProBaseController {
@GetMapping(value = "zlbWxLoginTest")
@ApiOperation(value = "测试浙里办微信小程序登录接口", notes = "测试浙里办微信小程序登录接口后端接口")
public String zlbWxLoginTest(@RequestParam @ApiParam(name = "st", value = "浙里办 ticketId", required = true)String st) {
try {
return buildResultStr(buildSuccessResultData(userService.getUserBeanByTicketId(st)));
}catch (Exception e) {
logError(log, e);
return buildResultStr(buildErrorResultData(e.getMessage()));
}
}
}
(6) UserServiceImpl 浙里办用户体系转换
/**
* 用户表(User)表服务实现类
*
* @author hejun
* @since 2022-02-22 10:02:16
*/
@Service("userService")
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private AuthService authService;
@Override
public UserBean getUserBeanByTicketId(String ticketId){
UserBean userBean = new UserBean();
//1. 通过ticketId 换取 accessToken
String token = authService.getTokenByTicketId(ticketId);
//3. 通过accessToken 获取用户信息
JSONObject userInfo = authService.getUserInfoByToken(token);
JSONObject personInfo = userInfo.getJSONObject("personInfo");
String phone = personInfo.get("phone").toString();
userBean.setMobile(phone);
userBean.setUsername(personInfo.get("userName").toString());
userBean.setIdnum(personInfo.get("idNo").toString());
userBean.setUserid(personInfo.get("userId").toString());
String login = null;
if (StringUtils.isNotNullString(phone)){
login = this.login(phone);
userBean.setToken(login);
log.info("token------------------------------", login);
}
return userBean;
}
/**
* 通过手机号登录
*
* @return token
*/
@Override
public String login(String phone) {
User user = this.getUserByPhone(phone);
if (user == null){
user = new User();
user.setMobile(phone);
this.insert(user);
}
//生成token
String token = getUserRsid(phone);
token = token.replaceAll("/","_");
//token放入缓存
JedisUtils.setObject(token,user,portalRsidCacheSeconds);
//返回token
return token;
}
}
六、接口调用方式
请以POST方式提交请求,参数以application/json形式提交。
“浙里办”单点登录,HTTP请求都必须在请求头(HTTP Header)中设置如下4个参数:
参数名 | 是否必填 | 类型 | 说明 |
X-BG-HMAC-SIGNATURE | 是 | string | API输入参数签名结果 |
X-BG-HMAC-ALGORITHM | 是 | string | 签名的摘要算法,当前仅支持hmac-sha256。 |
X-BG-HMAC-ACCESS-KEY | 是 | string | 分配给应用的accessKey,例如:12345678。 |
X-BG-DATE-TIME | 是 | string | 时间戳,时区为GMT+8,格式为:Tue, 09 Nov 2021 08:49:20 GMT。服务端允许客户端请求最大时间误差为100秒。 |
其中X-BG-HMAC-SIGNATURE的计算公式为:
signature = HMAC-SHA256-HEX(secret_key,signing_string) |
各字段解释如下:
- secret_key为接口申请完成后获取到的secret_key
- signing_string由请求方法、URI、请求参数等拼接获得,具体如下:
HTTP_METHOD+\n+HTTP_URI+\n+QUERY_STREAM+\n+X-BG-HMAC-ACCESS-KEY+\n+X-BG-DATE+\n |
参数解释如下图,详细代码可参考签名计算代码
参数名 | 说明 |
HTTP METHOD | 指 HTTP 协议中定义的 GET、PUT、POST 等请求方法,必须使用全大写的形式。 |
HTTP URI | 请求路径,要求必须以“/”开头,不以“/”开头的需要补充上,空路径为“/” |
X-BG-DATE | 请求头中的 Date ( GMT 格式 )格式为:“Tue, 09 Nov 2021 08:49:20 GMT” |
QUERY_STREAM | 是对于 URL 中的 query( query 即 URL 中?后面的 key1=valve1&key2=valve2 字符串)进行编码后的结果。以 key 按照字典顺序( ASCII 码由小到大)排序,并使用 & 符号连接起来,生成相应的query_string。 |
/uc/sso/access_token
参数 | 类型 | 描述 |
ticketId | String | 单点登录票据 |
appId | String | AppId |
参数 | 类型 | 描述 |
errorCode | String | 错误码 |
errorMsg | String | 错误信息 |
success | Boolean | 请求是否成功 |
data | Object | 响应体 |
|- accessToken | String | 获取用户信息token |
错误码 | 描述 |
C-USER-SSO-TICKET-INVALID | ticket非法 |
-
- 基于token获取用户信息
- 请求地址
/uc/sso/getUserInfo
参数 | 类型 | 描述 |
token | String | 获取用户信息token |
参数 | 类型 | 描述 |
success | Boolean | 请求是否成功 |
errorCode | String | 错误码 |
errorMsg | String | 错误信息 |
data | Object | 响应体 |
|- userType | String | 用户类型,PERSON 个人/LEGAL_PERSON 法人 |
|- personInfo | Object | 个人用户信息,当前登陆自然人的信息 |
|-- userId | String | 主键 |
|-- userName | String | 个人姓名 |
|-- idType | String | ID_CARD:身份证,PASSPORT:护照,OFFICER_CARD:军官证,MAINLAND_TRAVEL_PERMIT_FOR_HONGKONG_AND_MACAO_RESIDENTS:港澳居民来往内地通行证,MAINLAND_TRAVEL_PERMIT_FOR_TAIWAN_RESIDENTS:台湾居民来往大陆通行证,FOREIGN_PERMANENT_RESIDENT_ID_CARD:外国人永久居留身份证,FOREIGN_PASSPORT:外籍人士护照,DIPLOMACY_PASSPORT:外交护照,OFFICIAL_PASSPORT:公务护照,SOLDIER_CARD:士兵证,OFFICER_RETIRE_CARD:军官离退休证,GANG_AO_TAI_RESIDENCE_CART:港澳台居民居住证,GANG_AO_ID_CART:港澳居民身份证,UNIFIED_SOCIAL_ID:统一社会信用代码,OTHER:其他 |
|-- outerIdType | String | 外部证件类型 |
|-- idNo | String | 证件编号 |
|-- attnUserType | String | 法人经办人时用户类型,评级 |
|-- phone | String | 手机号 |
| String | 邮箱 |
|-- nation | String | 民族 |
|-- gender | String | 性别 |
|-- birthday | String | 生日 |
|-- certKey | String | 身份散列值 |
|-- attributes | Object | 额外属性 |
|- legalPersonInfo | Object | 法人用户信息,比如公司相关的信息 |
|-- name | String | 法人名称 |
|-- unifiedSocialId | String | 社会统一信用代码 |
|-- orgType | String | 法人类型 |
|-- attnName | String | 经办人姓名 |
|-- attnPhone | String | 经办人手机号 |
|-- attnIdType | String | 经办人证件类型 |
|-- attnIdNo | String | 经办人证件号码 |
|-- attnUserType | String | 经办人用户等级 |
|-- principal | String | 法人代表人姓名 |
|-- gender | Integer | 法人代表人性别 |
|-- nation | Integer | 法人代表人民族 |
|-- idType | Integer | 法人代表人证件类型 |
|-- outerIdType | String | 法人代表人外部证件类型 |
|-- idNo | String | 法人代表人证件号码 |
|-- principalUserId | String | 法人代表唯一键 |
|-- corpId | String | 法人唯一键 |
|-- attributes | Object | 额外属性 |
|- organizationInfoList | Array | 所属组织信息 |
|-- orgId | String | 组织主键 |
|-- oid | String | Alias for orgId |
|-- parentId | String | 父组织主键 |
|-- pid | String | Alias for parentId |
|-- name | String | 组织机构简称 |
|--fullName | String | 组织机构全称 |
|--devCoding | String | 组织后缀 |
|--leafFlag | Boolean | 是否叶子标志 |
|--orderBy | Integer | 排序号,从小到大 |
错误码 | 描述 |
C-USER-SSO-TOKEN-INVALID | token非法 |
C-USER-SSO-USER-EMPTY | 用户信息为空 |