一、概述
本指南旨在为“浙里办”单点登录组件提供接入指南,“浙里办”单点登陆组件,上架在IRS,为上架在IRS的应用,提供统一的单点登录解决方案,现阶段仅支持微信端的接入。
二、服务创建
IRS 应用管理员在 IRS 应用发布服务侧进行“应用发布注册”。
注意事项
1、IRS 应用系统查询没有应用信息,可通过 IRS 应用编目查询该应用的 IRS 应用管理员
信息。
2、IRS 应用管理员添加的开发商信息需要有浙政钉账号,由服务侧应用建设单位提供,
用于 IRS 应用发布开发商工作台进行服务部署。
3、应用接入“浙里办”单点登录组件前,需要先获取AK&SK。

三、接入说明
接入前需要服务侧应用建设单位已完成在 IRS 应用编目工作。从开发部署到服务上架,需
要经历以下主要阶段:
  
四、接入规范
- 接入“浙里办”微信小程序的 H5 应用(以下简称应用), 应当符合“同源发布”及无障碍适老化等要求。
- 应用应当在浙江省一体化数字资源系统(以下简称 IRS)发布,并使用统一的域名 https://mapi.zjzwfw.gov.cn/,作为接入微信小程序的前置条件。应用上架 IRS,应遵循 IRS 相关规范。
- 应用接入“浙里办”微信小程序,应当按照本指南操作步骤与注意事项,进行微信端的兼容适配。
五、操作步骤
1、单点登录适配
政务服务网法人用户单点登录
支持范围
“浙里办”APP、浙江政务服务网
登录入口
https://esso.zjzwfw.gov.cn/opensso/spsaehandler/metaAlias/sp?spappurl=回调地址
浙江政务服务网登出地址
https://esso.zjzwfw.gov.cn/opensso/UI/Logout?goto=https://oauth.zjzwfw.gov.cn/
oauth/logout.do?redirect=登录地址
API 概览
| 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 的信息。
注意事项
“浙里办”APP 没有支持浙江政务服务网法人单点登录测试环境
https://essotest.zjzwfw.gov.cn 地址免登能力;
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。 | 
- 接口说明 
  - 基于单点登录票据换取请求token
 
-  
  - 请求地址
 
/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 | 用户信息为空 | 










