一、概述
本指南旨在为“浙里办”单点登录组件提供接入指南,“浙里办”单点登陆组件,上架在IRS,为上架在IRS的应用,提供统一的单点登录解决方案,现阶段仅支持微信端的接入。
二、服务创建
3、应用接入“浙里办”单点登录组件前,需要先获取AK&SK。
三、接入说明
四、接入规范
- 接入“浙里办”微信小程序的 H5 应用(以下简称应用), 应当符合“同源发布”及无障碍适老化等要求。
- 应用应当在浙江省一体化数字资源系统(以下简称 IRS)发布,并使用统一的域名 https://mapi.zjzwfw.gov.cn/,作为接入微信小程序的前置条件。应用上架 IRS,应遵循 IRS 相关规范。
- 应用接入“浙里办”微信小程序,应当按照本指南操作步骤与注意事项,进行微信端的兼容适配。
五、操作步骤
1、单点登录适配
API
|
接口说明
|
访问地址
|
atg.biz.userquery |
验证令牌并
获取用户的
登录信息
|
政务外网地址:
https://bcdsg.zj.gov.cn:8443/restapi/prod/IC
33000020220309000001/rest/user/query
|
互联网地址:
https://ibcdsg.zj.gov.cn:8443/restapi/prod/I
C33000020220309000001/rest/user/query
| ||
atg.biz.callb
ackurl
|
业务系统回
调地址添加
|
政务外网地址:
https://bcdsg.zj.gov.cn:8443/restapi/prod/IC
33000020220309000002/rest/callbackUrl
|
互联网地址:
https://ibcdsg.zj.gov.cn:8443/restapi/prod/I
C33000020220309000002/rest/callbackUrl
|
- IRS 应用管理员在 IRS 申请【浙江政务服务网个人单点登录】组件,前端通过调用登录地址获取 ticket 票据后,服务端可通过 ticketvalidation 和 getuserinfo接口。
- 登录地址 spappurl 参数回调地址用于接收 ssotoken 的信息。
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。 |
参数 |
类型 |
描述 |
ticketId |
String |
单点登录票据 |
appId |
String |
AppId |
参数 |
类型 |
描述 |
errorCode |
String |
错误码 |
errorMsg |
String |
错误信息 |
success |
Boolean |
请求是否成功 |
data |
Object |
响应体 |
|- accessToken |
String |
获取用户信息token |
错误码 |
描述 |
C-USER-SSO-TICKET-INVALID |
ticket非法 |
参数 |
类型 |
描述 |
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_*_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 |
用户信息为空 |