电子政务系统的扫码登录——国密加密加轮询的完整实现
电子政务系统的扫码登录——国密加密+轮询的完整实现
不是微信扫码、不是钉钉扫码。是政务系统自己的电子社保卡扫码登录——后台调一卡通平台接口,国密SM4加密通信,前端轮询等待用户手机确认。每一步都是生产环境跑过的代码。
文章目录
- 电子政务系统的扫码登录——国密加密+轮询的完整实现
- 一、为什么政务系统需要扫码登录
- 二、完整流程:四个角色协作
- 三、获取二维码——国密SM4加密通信
- 四、轮询扫码结果——等待手机端确认
- 五、前端轮询——每2秒查一次结果
- 六、后端登录处理——身份证号反查操作员
- 七、二维码过期与超时控制
- 八、安全要点
- 8.1 国密SM4全链路加密
- 8.2 二维码值(ewmz)一次性
- 8.3 登录事件审计
- 九、结语
一、为什么政务系统需要扫码登录
政务经办系统的操作员每天登录几十次——上午登进去办几个业务,出去开个会,回来会话过期又要重新输密码。密码是大小写+数字+特殊字符,输错三次锁账户,解锁要找管理员。
扫码登录解决的正是这个痛点:手机上有电子社保卡APP,打开扫一下电脑屏幕上的二维码,直接登录。不需要记密码,不需要找管理员重置。
二、完整流程:四个角色协作
浏览器(登录页) 后台(eSiCardLogin) 一卡通平台 │ │ │ │ ① GET ?type_=(空) │ │ │─────────────────────────────→│ │ │ │ ② getQrCode() │ │ │──────────────────────────────→│ │ │ ③ 返回二维码数据(token) │ │ ④ JSON: {qrcode数据} │←──────────────────────────────│ │←─────────────────────────────│ │ │ │ │ │ ⑤ qrcode.min.js生成二维码 │ │ │ 用户手机扫码确认 │ │ │ │ │ │ ⑥ GET ?type_=getResult&ewmz= │ │ │─────── 每2秒轮询一次 ───────→│ │ │ │ ⑦ getResult(ewmz) │ │ │──────────────────────────────→│ │ │ ⑧ 返回身份证号(aac002) │ │ │←──────────────────────────────│ │ │ ⑨ 查user表获取操作员信息 │ │ │ ⑩ 写入session + 代办任务 │ │ ⑪ JSON: {"code":"00"} │ │ │←─────────────────────────────│ │ │ ⑫ 跳转主页面 │ │三、获取二维码——国密SM4加密通信
publicStringgetQrCode()throwsException{Stringuser="XXXXX";// 应用编码,从配置文件读取// 请求参数——服务编码、渠道编号、超时时间、随机数Stringparam="{"+"\"header\":{\"appcode\":\""+user+"\","+"\"servicecode\":\"10138200\","+"\"paramin\":\"渠道编号\","+"\"overtime\":\"10\","+"\"random\":\"1590022782161\"},"+"\"body\":{\"aaz570\":\"XXXXXXXXXX\"}"+"}";Stringurl=configUtil.get("ykt_url");Stringkey=configUtil.get("sm4_key");// SM4密钥从配置文件读取SmUtilfu=newSmUtil();// SM4加密——经UTF-8转码用SM4加密再转16进制Stringjson="{"+"\"appcode\":\""+user+"\","+"\"security\":\""+fu.sm4Add16(key,param)+"\""+"}";// HTTP POST 发送加密请求HttpURLConnectionconn=(HttpURLConnection)(newURL(url)).openConnection();conn.setRequestMethod("POST");conn.setDoOutput(true);conn.setRequestProperty("Content-Type","application/json;charset=utf-8");conn.connect();BufferedWriterout=newBufferedWriter(newOutputStreamWriter(conn.getOutputStream(),"utf-8"));out.write(json);out.flush();// 读取响应——加密返回BufferedReaderin=newBufferedReader(newInputStreamReader(conn.getInputStream(),"UTF-8"));Stringline;StringBuildersb=newStringBuilder();while((line=in.readLine())!=null)sb.append(line);// SM4解密返回数据JSONObjectobject=JSONObject.fromObject(sb.toString());StringJsonencrypt=object.getString("encrypt");Stringdelencrypt=fu.sm4Del16(key,Jsonencrypt);JSONObjectout_object=JSONObject.fromObject(delencrypt);returnout_object.get("body").toString();}请求参数明文→SM4加密→16进制→发送。返回数据→SM4解密→解析JSON。全程国密,符合政务系统信创合规要求。
服务编码10138200是一卡通平台的"获取二维码"接口。不同的服务编码代表不同的业务——10138201是轮询扫码结果的接口。
四、轮询扫码结果——等待手机端确认
publicStringgetResult(Stringewmz)throwsException{// 相同的加密流程,不同的服务编码和参数Stringparam="{"+"\"header\":{\"appcode\":\""+user+"\","+"\"servicecode\":\"10138201\","// 查询扫码结果的服务编码+"\"paramin\":\"渠道编号/二维码值\","+"\"overtime\":\"20\","+"\"random\":\"123456\"},"+"\"body\":{\"aaz570\":\"9400000303\","+"\"ewmz\":\""+ewmz+"\"}"// 传入二维码值+"}";// SM4加密 → 发送 → SM4解密 → 解析// ... 相同加密流程 ...JSONObjectout_object=JSONObject.fromObject(delencrypt);Stringoutstr=out_object.getString("body");out_object=JSONObject.fromObject(outstr);returnout_object.get("aac002").toString();// 返回身份证号}用户手机扫码后,一卡通平台把二维码值和用户身份绑定。后台轮询时传入相同的ewmz(二维码值),平台返回该用户的身份证号。
服务编码10138201的超时时间设为20秒——比获取二维码的10秒更长,因为用户扫码确认需要时间。
五、前端轮询——每2秒查一次结果
// 获取二维码$.get("/eSiCardLogin",function(data){// 用qrcode.min.js生成二维码图片varqrcode=newQRCode(document.getElementById("qrcode"),{text:data.qrcode,// 二维码内容width:200,height:200});// 开始轮询——每2秒查一次vartimer=setInterval(function(){$.get("/eSiCardLogin?type_=getResult&ewmz="+data.ewmz,function(result){if(result.code==="00"){clearInterval(timer);window.location.href="/pages/menu/mainframe.jsp";}});},2000);});轮询不是一直轮——有超时控制。二维码过期时间通常设为60秒,超时了前端清除定时器,提示"二维码已过期,请刷新"。
六、后端登录处理——身份证号反查操作员
if("getResult".equals(param)){Stringewmz=request.getParameter("ewmz");Stringaac002=obj.getResult(ewmz);// 从平台拿到身份证号Useruser=newUser();user.setPsn_idcard(aac002);// 用身份证号查操作员List<User>list=(List<User>)DBUtil.getDao(userMapper.class,"getUserByID",user);if(list.size()==1){UseruserObj=list.get(0);userObj.setIp(this.getIpAddr(request));userObj.setSessionid(session.getId());// 记录登录事件(审计用)DBUtil.SaveOne(userMapper.class,"saveloginevent",userObj);// 写入sessionsession.setAttribute("user",userObj);// 加载待办任务loadPendingTasks(userObj,session);// 返回成功response.getWriter().print("{\"msg\":\"登陆成功\",\"code\":\"00\"}");}}一卡通平台返回的是身份证号(aac002),不是系统内部的用户编号。所以需要一个getUserByID(身份证号反查)的Mapper方法从op_person表查到这个身份证号对应的经办操作员。
登录成功后记录登录事件——谁、什么时间、从哪个IP登录。不是多此一举,是政务系统的审计标配。
七、二维码过期与超时控制
整个扫码登录的时间窗口由三个参数控制:
| 参数 | 位置 | 作用 |
|---|---|---|
overtime | 请求参数 | 一卡通平台的二维码有效期 |
| 轮询间隔 | 前端JS | 每2秒问一次"扫了没" |
| 轮询总时长 | 前端JS | 60秒后自动终止,提示刷新 |
这三个参数配合起来就是一次完整的扫码登录生命周期:
0s → 获取二维码,有效期60秒 2s → 第1次轮询…未扫 4s → 第2次轮询…未扫 ... 30s → 第15次轮询…用户已扫,登录成功 60s → 超时终止,提示刷新二维码八、安全要点
8.1 国密SM4全链路加密
从请求参数到返回数据全程SM4加密。即使HTTP抓包拿到请求体,看到的也是密文。密钥存在服务端配置文件里,前端不可见。
8.2 二维码值(ewmz)一次性
ewmz是一卡通平台生成的随机token,扫码确认后立即作废。截取当前页的二维码值无法重放——它绑定了当时生成的二维码,过期或已使用后一卡通平台不再返回用户信息。
8.3 登录事件审计
每次扫码登录成功,记录saveloginevent——操作员、登录时间、IP地址、sessionid。和传统的密码登录走同一条审计日志。
九、结语
政务系统的扫码登录和互联网产品不一样——后台不是调微信/支付宝的开放API,是调各地人社部门的一卡通平台。加密不是TLS就够,还要加一层国密SM4。前端不是WebSocket长连接,是最朴素的短轮询。
这套机制从2021年上线跑到系统下线,没出过安全事故。不是因为用了多高级的技术——是因为每一条通信链路都加密了,每一个登录都记录在案了,每一个环节都能追溯。