小言_互联网的博客

浙政钉2.0免登

662人阅读  评论(0)

写在前面:

目前主要做的是政府一块类的项目,其中大大小小项目免不了通过浙政钉来访问进而可以实现免登操作。其实看了之后还蛮简单的,最近项目中又用到了,趁此机会打卡记录学习一下。为啥是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,具体看实际项目,一般都有官网去申请。像本次浙政钉免登,就需要去钉钉官网申请服务上架。有详细说明,该准备什么材料就准备什么材料。一般让项目经理去做。开发只需拿到申请下来的账号和密码即可,要么还有一份技术对接文档。实际如下得到一个域名地址和应用账号密码(配置文件中加起)


   
  1. dtalk:
  2. domainname: XXXXXX
  3.   appkey:  XXXXXX
  4.   appsecret: XXXXXX

从后台开发角度:

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

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


   
  1. 换取用户信息时需要用到访问应用的AccessToken,做成 2个Service
  2. public interface UserDtalkService extends IService<UserDtalk> {
  3. /**
  4.      * 政务钉钉-获取AccessToken     
  5. */
  6. String getAccessToken();
  7.       /**
  8. * 根据authCode换取用户信息
  9.      * @param authCode
  10. */
  11. JSONObject getDingtalkAppUser(String authCode);
  12. }
  13. --------------------------IMPL实现类----------------------------- 
  14. getClient请求方法。
  15. public PostClient getClient(String api){
  16. ExecutableClient executableClient =ExecutableClient.getInstance();
  17. executableClient.setAccessKey(appKey);
  18. executableClient.setSecretKey(appSecret);
  19. executableClient.setDomainName(domaiNname);
  20. executableClient.setProtocal( "https");
  21. executableClient.init();
  22. return executableClient.newPostClient(api);
  23. }
  24. 创建一个缓存,用于存储accessToken,不存在则重新获取。
  25. 其中的appKey和appSecret是之前上架时提供的。
  26. /**
  27. * 政务端-浙政钉accessToken
  28. */
  29. public final static String GOV_DTALK_ACCESS_TOKEN = "gov_dtalk_access_token";
  30. @CreateCache(name = "govuser.accesstoken", cacheType = CacheType.REMOTE)
  31. private Cache<String, String> accessTokenCache;
  32. public String getAccessToken() {
  33. //判断缓存中accessToken是否存在
  34. String redisAccessToken = accessTokenCache.get(GOV_DTALK_ACCESS_TOKEN);
  35. if (StringUtils.isNotBlank(redisAccessToken)){
  36. return redisAccessToken;
  37. }
  38. try {
  39. //缓存中不存在,则重新获取
  40. String api = "/gettoken.json";
  41. PostClient client = this.getClient(api);
  42. client.addParameter( "appkey", appKey);
  43. client.addParameter( "appsecret", appSecret);
  44. //调用API
  45. String apiResult = client.post();
  46. log.info( "getAccessToken返回结果打印:"+apiResult);
  47. JSONObject jsonObject = JSONObject.parseObject(apiResult);
  48. if (jsonObject != null && jsonObject.getBoolean( "success")){
  49. JSONObject contentObj = jsonObject.getJSONObject( "content");
  50. JSONObject dataObj = contentObj.getJSONObject( "data");
  51. String accessToken = dataObj.getString( "accessToken");
  52. long expiresIn = dataObj.getLong( "expiresIn");
  53. accessTokenCache.put(GOV_DTALK_ACCESS_TOKEN, accessToken, expiresIn, TimeUnit.SECONDS);
  54. return accessToken;
  55. }
  56. } catch (Exception e){
  57. log.error( "浙政钉-获取accessToken异常",e);
  58. }
  59. return null;
  60. }
  61. 根据authCode换取用户信息。也会去调用getAccessToken()方法拿Token。
  62. public JSONObject getDingtalkAppUser(String authCode) {
  63. try {
  64. String api = "/rpc/oauth2/dingtalk_app_user.json";
  65. PostClient postClient = this.getClient(api);
  66. postClient.addParameter( "access_token", this.getAccessToken());
  67. postClient.addParameter( "auth_code", authCode);
  68. String apiResult = postClient.post();
  69. log.info( "getDingtalkAppUser返回结果打印:"+apiResult);
  70. JSONObject jsonObject = JSONObject.parseObject(apiResult);
  71. if (jsonObject != null && jsonObject.getBoolean( "success")){
  72. JSONObject contentObj = jsonObject.getJSONObject( "content");
  73. JSONObject dataObj = contentObj.getJSONObject( "data");
  74. return dataObj;
  75. }
  76. } catch (Exception e){
  77. log.error( "浙政钉-根据authCode换取用户信息异常",e);
  78. }
  79. return null;
  80. }

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

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


   
  1. CREATE TABLE `user_dtalk` (
  2. `id` int( 11) NOT NULL AUTO_INCREMENT,
  3. `dtalk_id` varchar( 128) DEFAULT NULL COMMENT '钉钉账号id',
  4. `bind_user_id` int( 20) DEFAULT NULL COMMENT '系统用户账号id',
  5. `dtalk_user_info` text COMMENT '钉钉用户信息',
  6. `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  7. `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  8. `bind_user_name` varchar( 255) DEFAULT NULL COMMENT 系统用户名 ',
  9. PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

   
  1. /**
  2. * 浙政钉2.0-首次登陆,绑定用户
  3. * @param bindDataForm
  4. */
  5. @GetMapping("loginAndBind")
  6. public Result loginAndBind(BindDataForm bindDataForm) {
  7. return userDtalkService.doBindUser(bindDataForm);
  8. }
  9. bindDataForm入参中主要是userName、password、authCode。
  10. 系统用户名密码以及前端调钉钉获取的授权码。

具体Impl:

大致按照如下步骤实现

①拿钉钉唯一accountId

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

③返回token

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

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


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

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


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

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


   
  1. // 判断账号密码
  2. if (StringUtil.isEmptyOrNull(bindDataForm.getPassword())) {
  3. return Result.fail( "密码不能为空");
  4. }
  5. if (StringUtil.isEmptyOrNull(bindDataForm.getUserName())) {
  6. return Result.fail( "账号不能为空");
  7. }
  8. // 获取账号信息
  9. Result<User> res = organizationProvider.getUserByUniqueId(bindDataForm.getUserName());
  10. User userFromAdmin = res.getData();
  11. if (userFromAdmin == null) {
  12. return Result.fail( "未查询到登陆用户");
  13. }
  14. // 判断密码是否正确
  15. BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
  16. if (!bcryptPasswordEncoder.matches(bindDataForm.getPassword(),userFromAdmin.getPassword())) {
  17. return Result.fail( "账号密码错误");
  18. }

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


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

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


   
  1. UserDtalk oldRecord = baseMapper.getByUserIdAndDtalkId(userFromAdmin.getId(), accountId);
  2. if (Objects.nonNull(oldRecord)) {
  3. oldRecord.setDtalkUserInfo(userDtalk.getDtalkUserInfo());
  4. oldRecord.setBindUserName(userDtalk.getBindUserName());
  5. baseMapper.updateById(oldRecord);
  6. } else {
  7. this.save(userDtalk);
  8. }

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


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

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


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

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

接下来就是

免登接口:

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

具体Impl:

大致按照如下步骤实现

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

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

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

1) 根据authCode拿accountId


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

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


   
  1. UserDtalk userDtalk = this.getOne(new QueryWrapper<UserDtalk>().e q("dtalk_id", accountId));
  2. if (userDtalk == null) {
  3. return Result.fail(SystemErrorType.DTALK_UNBIND, "暂未绑定用户,请先绑定");
  4. }

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

Map token = tokenService.getTokenFromCache(userDtalk);

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


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

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


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

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

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

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


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


初遇ZooKeeper



转载:https://blog.csdn.net/Boom_404/article/details/114959157
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场