浙政钉2.0免登

您所在的位置:网站首页 钉钉用户名登录怎么修改 浙政钉2.0免登

浙政钉2.0免登

2024-01-17 20:33| 来源: 网络整理| 查看: 265

写在前面:

目前主要做的是政府一块类的项目,其中大大小小项目免不了通过浙政钉来访问进而可以实现免登操作。其实看了之后还蛮简单的,最近项目中又用到了,趁此机会打卡记录学习一下。为啥是2.0,现在和之前的浙政钉免登有点改动了,统一都用2.0这套。

什么是浙政钉?

浙政钉是浙江省政府省、市、县(市、区)、乡镇、村(街道、社区)五级政府人员的组织,是浙江省政府数字化转型的沟通协同平台。深化“最多跑一次”改革推进政府数字化转型。为规范浙政钉整体架构体系,按照统分结合原则,由省政府办公厅统一设计整体工作界面和系统框架,统筹指导全省统建应用建设,各单位根据自身业务特点分别建设自建应用,最终形成全省统一的政府系统掌上协同办公平台。

日常网页中的“记住密码”就是一次免登:   

流程:用户登录时,输入用户名、密码等等,再勾“记住密码”选项,成功登录过一次系统,而用户的用户名以及密码等等都保存在cookie中,当用户再次登录时,系统会自动调用Cookie中的数据,自动给用户赋值,从而实现免登陆。

浙政钉免登:

用户通过浙政钉app去应用平台上登录某个系统,第一次登录时,需要输入账号密码,第二次再来进入时,不需要再输入账号密码即可正常登录系统。

“记住密码”VS浙政钉免登 相同点:

1.两者在首次登录时,都需要输入账号和密码。

2. 二次登录时,有了首次登录,即可实现免登操作。

“记住密码”VS浙政钉免登 不同点:

1.“记住密码”账号密码信息保存于浏览器cookie中,一旦清除浏览器缓存,下次还得重新输入账号密码。

2.浙政钉免登登入者信息保存于系统用户表中,因为通过浙政钉进行的登录,拿到钉钉的这个用户信息,比对系统的用户表有没有绑定登录者用户的accountID(钉钉id),有过绑定,说明之前登录过。不需要输入账号密码即可实现登入。

综合得出结论,实现浙政钉免登步骤:

      1. 首次登录需要输入账号密码

      2. 登录成功,进行绑定(绑定登录者用户accountID(钉钉id)和系统用户表进行绑定)

      3. 二次登录时,判断登录者accountID(钉钉id)是否和系统用户表有过绑定

      4. 有过绑定,即返回token无需再输账号密码。否则,跳转到登录页,手动进行账号密码登录。

Ps:现在项目是SpringCloud微服务使用Nacos做注册中心。登录有专属的一个authentication-server服务,进行登录的校验。登录成功,返回一个token给前端。

 

没实际上手操作前:

1.不明觉厉,手指点点就能把输入账号密码等一系列流程舍去就能登录成功。

2.好奇于其背后的实现逻辑。是否很复杂?很高大上?能否自己也照搬免登成功?

深入研究一番:

原来这么简单,把它想象成日常用账号密码登录即可,只不过前面多加一步,判断之前是否绑定过即可。绑定过,调登录认证服务的接口,没绑定过,提示“暂未绑定,无法免登”。

前期准备工作:

涉及到这种第三方对接请求之类的,肯定要去申请对应的appkey和appsecret,具体看实际项目,一般都有官网去申请。像本次浙政钉免登,就需要去钉钉官网申请服务上架。有详细说明,该准备什么材料就准备什么材料。一般让项目经理去做。开发只需拿到申请下来的账号和密码即可,要么还有一份技术对接文档。实际如下得到一个域名地址和应用账号密码(配置文件中加起)

dtalk: domainname: XXXXXX   appkey: XXXXXX   appsecret: XXXXXX

从后台开发角度:

       根据上面提到的步骤。无非就2个接口。一个首次登陆,绑定用户接口。另一个免登接口。

Ps:免登不仅仅是后台的活,前端也需要调这个申请上架后应用的一个authCode。放在接口请求参数中带过来。原因就是保证,根据authCode换取用户信息,免登过程中是项目里的免登操作,校验前后双方都是用的同一个上架的应用。也提高了这中间的安全性。

换取用户信息时需要用到访问应用的AccessToken,做成2个Service public interface UserDtalkService extends IService { /**      * 政务钉钉-获取AccessToken      */ String getAccessToken();      /** * 根据authCode换取用户信息      * @param authCode */ JSONObject getDingtalkAppUser(String authCode); } --------------------------IMPL实现类-----------------------------  getClient请求方法。 public PostClient getClient(String api){ ExecutableClient executableClient =ExecutableClient.getInstance(); executableClient.setAccessKey(appKey); executableClient.setSecretKey(appSecret); executableClient.setDomainName(domaiNname); executableClient.setProtocal("https"); executableClient.init(); return executableClient.newPostClient(api); } 创建一个缓存,用于存储accessToken,不存在则重新获取。 其中的appKey和appSecret是之前上架时提供的。 /** * 政务端-浙政钉accessToken */ public final static String GOV_DTALK_ACCESS_TOKEN = "gov_dtalk_access_token"; @CreateCache(name = "govuser.accesstoken", cacheType = CacheType.REMOTE) private Cache accessTokenCache; public String getAccessToken() { //判断缓存中accessToken是否存在 String redisAccessToken = accessTokenCache.get(GOV_DTALK_ACCESS_TOKEN); if (StringUtils.isNotBlank(redisAccessToken)){ return redisAccessToken; } try { //缓存中不存在,则重新获取 String api = "/gettoken.json"; PostClient client = this.getClient(api); client.addParameter("appkey", appKey); client.addParameter("appsecret", appSecret); //调用API String apiResult = client.post(); log.info("getAccessToken返回结果打印:"+apiResult); JSONObject jsonObject = JSONObject.parseObject(apiResult); if (jsonObject != null && jsonObject.getBoolean("success")){ JSONObject contentObj = jsonObject.getJSONObject("content"); JSONObject dataObj = contentObj.getJSONObject("data"); String accessToken = dataObj.getString("accessToken"); long expiresIn = dataObj.getLong("expiresIn"); accessTokenCache.put(GOV_DTALK_ACCESS_TOKEN, accessToken, expiresIn, TimeUnit.SECONDS); return accessToken; } }catch (Exception e){ log.error("浙政钉-获取accessToken异常",e); } return null; } 根据authCode换取用户信息。也会去调用getAccessToken()方法拿Token。 public JSONObject getDingtalkAppUser(String authCode) { try { String api ="/rpc/oauth2/dingtalk_app_user.json"; PostClient postClient = this.getClient(api); postClient.addParameter("access_token", this.getAccessToken()); postClient.addParameter("auth_code", authCode); String apiResult = postClient.post(); log.info("getDingtalkAppUser返回结果打印:"+apiResult); JSONObject jsonObject = JSONObject.parseObject(apiResult); if (jsonObject != null && jsonObject.getBoolean("success")){ JSONObject contentObj = jsonObject.getJSONObject("content"); JSONObject dataObj = contentObj.getJSONObject("data"); return dataObj; } }catch (Exception e){ log.error("浙政钉-根据authCode换取用户信息异常",e); } return null; }

首次登陆,绑定用户接口:

        创表user_dtalk绑定记录表:绑定钉钉用户和系统用户

CREATE TABLE `user_dtalk` ( `id` int(11) NOT NULL AUTO_INCREMENT, `dtalk_id` varchar(128) DEFAULT NULL COMMENT '钉钉账号id', `bind_user_id` int(20) DEFAULT NULL COMMENT '系统用户账号id', `dtalk_user_info` text COMMENT '钉钉用户信息', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `bind_user_name` varchar(255) DEFAULT NULL COMMENT 系统用户名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /** * 浙政钉2.0-首次登陆,绑定用户 * @param bindDataForm */ @GetMapping("loginAndBind") public Result loginAndBind(BindDataForm bindDataForm) { return userDtalkService.doBindUser(bindDataForm); } bindDataForm入参中主要是userName、password、authCode。 系统用户名密码以及前端调钉钉获取的授权码。

具体Impl:

大致按照如下步骤实现

①拿钉钉唯一accountId

②校验当前用户账号和密码,进行绑定

③返回token

1) 拿参数里的authCode去换取用户信息(就是上面Service中getDingtalkAppUser方法)

2) 获取钉钉用户信息成功,能得到其钉钉唯一的accountId。

JSONObject userData = this.getDingtalkAppUser(bindDataForm.getAuthCode()); if (userData == null) { return Result.fail("获取钉钉用户信息失败"); } String accountId = userData.getString("accountId");

3) 新建一个UserDtalk对象,将上面得到的accountId和钉钉用户详情JSON进行赋值。

UserDtalk userDtalk = new UserDtalk(); userDtalk.setDtalkId(accountId); userDtalk.setDtalkUserInfo(FastJsonUtil.toJSONString(userData))

4) 根据入参的userName和password判断进行匹配,是否存在当前用户及密码是否匹配。Ps:项目里是微服务,认证登录啥的用的都是标准版的。开了提供用户名访问用户信息的接口。

// 判断账号密码 if (StringUtil.isEmptyOrNull(bindDataForm.getPassword())) { return Result.fail("密码不能为空"); } if (StringUtil.isEmptyOrNull(bindDataForm.getUserName())) { return Result.fail("账号不能为空"); } // 获取账号信息 Result res = organizationProvider.getUserByUniqueId(bindDataForm.getUserName()); User userFromAdmin = res.getData(); if (userFromAdmin == null) { return Result.fail("未查询到登陆用户"); } // 判断密码是否正确 BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder(); if (!bcryptPasswordEncoder.matches(bindDataForm.getPassword(),userFromAdmin.getPassword())) { return Result.fail("账号密码错误"); }

5) 账号存在,密码也匹配,把当前登录用户一部分信息也赋值到userDtalk对象去。

userDtalk.setBindUserId(userFromAdmin.getId()); userDtalk.setBindUserName(userFromAdmin.getUsername()); userDtalk.setCreateTime(new Date());

6) 做进一步校验,防止之前已经绑定过,后期系统账号信息做了更改。没同部更新到绑定记录表中。Ps:系统用户的id和钉钉id相当于做了主键,唯一判断。存在,则更新最新信息。不存在走保存。

UserDtalk oldRecord = baseMapper.getByUserIdAndDtalkId(userFromAdmin.getId(), accountId); if (Objects.nonNull(oldRecord)) { oldRecord.setDtalkUserInfo(userDtalk.getDtalkUserInfo()); oldRecord.setBindUserName(userDtalk.getBindUserName()); baseMapper.updateById(oldRecord); } else { this.save(userDtalk); }

7) 绑定数据入库之后,调用登录接口获取token。

Map token = tokenService.getTokenAndSaveToCache(bindDataForm); if (null == token) { return Result.fail("登录失败"); } return Result.success(token);

8) getTokenAndSaveToCache中主要是远程调用authentication-server服务来获取登录接口。返回token给前端。外加存储token至缓存,提高下次免登速度。

public Map getTokenAndSaveToCache(BindDataForm bindDataForm) { try { // 远程调用登录接口 Map loginResultMap = authenticationProvider.login(bindDataForm.getUserName(), bindDataForm.getPassword(), "password", "read"); // 存储token至缓存 userTokenCache.put(REDIS_KEY_PRO + String.valueOf(loginResultMap.get("user_id")), FastJsonUtil.toJSONString(loginResultMap), Long.parseLong(String.valueOf(loginResultMap.get("expires_in"))), TimeUnit.SECONDS); return loginResultMap; } catch (Exception e) { log.info("请求登录接口异常: username:{}", e, bindDataForm.getUserName()); return null; } }

至此首次登录-绑定用户这一步已经走通。

接下来就是

免登接口:

       上面绑定接口时已经参数中带了账号和密码,在免登这里只需要一个authCode授权码即可。因为,有getDingtalkAppUser方法根据authCode换取用户信息。如果上次绑定成功过后,系统用户id和钉钉id将存在于user_dtalk表中。因此,免登操作只需要去user_dtalk表中查询accountId是否存在。存在即代表上次绑定成功。不存在即还未绑定用户,前端进行友好提示。

具体Impl:

大致按照如下步骤实现

①根据authCode访问getDingtalkAppUser服务。拿钉钉唯一accountId。

②查询绑定表dtalk_id钉钉id是否存在

③调远程接口返回token。Ps:authentication-server中还提供了根据用户名登录的接口。

1) 根据authCode拿accountId

JSONObject userData = this.getDingtalkAppUser(authCode); if (userData == null) { log.info("获取钉钉用户信息失败"); return Result.fail("获取钉钉用户信息失败"); } String accountId = userData.getString("accountId");

2) 查看绑定表是否存在该accountId(钉钉id)

UserDtalk userDtalk = this.getOne(new QueryWrapper().eq("dtalk_id", accountId)); if (userDtalk == null) { return Result.fail(SystemErrorType.DTALK_UNBIND,"暂未绑定用户,请先绑定"); }

3) 已绑定,远程调用生成token。

Map token = tokenService.getTokenFromCache(userDtalk);

4) 先判断缓存中是否还存在token。可能上一步刚绑定完,下一步退出进来尝试免登操作。

public Map getTokenFromCache(UserDtalk userDtalk) { // 从缓存读取token if (null != userTokenCache.get(REDIS_KEY_PRO + userDtalk.getBindUserId())) { Map token = JsonUtil.toMapFromJsonStr(userTokenCache.get(REDIS_KEY_PRO + userDtalk.getBindUserId())); return token; } }

5) 缓存中不存在,过期了可能。再来远程调用根据用户名就能返回token的接口。同样的,提高下次免登速度。存一下缓存。

public Map getTokenFromCache(UserDtalk userDtalk) { try { // 远程调用登录接口 Map loginResultMap = authenticationProvider.loginByMobile(userDtalk.getBindUserName(), null, "mobile", "read"); // 存储token至缓存 userTokenCache.put(REDIS_KEY_PRO + String.valueOf(loginResultMap.get("user_id")), FastJsonUtil.toJSONString(loginResultMap), Long.parseLong(String.valueOf(loginResultMap.get("expires_in"))), TimeUnit.SECONDS); return loginResultMap; } catch (Exception e) { log.info("远程调用mobile类型登录接口异常: username={}", e, userDtalk.getBindUserName()); } }

至此,免登over。又一个点Get到了。

想看前面几期文章 请点击下列图片

我和我的项目之沙箱环境模拟支付宝支付(附演示视频)

我和我的项目之对接浙江政务服务网(法人)登录

初遇ZooKeeper



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3