浙政钉2.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,具体看实际项目,一般都有官网去申请。像本次浙政钉免登,就需要去钉钉官网申请服务上架。有详细说明,该准备什么材料就准备什么材料。一般让项目经理去做。开发只需拿到申请下来的账号和密码即可,要么还有一份技术对接文档。实际如下得到一个域名地址和应用账号密码(配置文件中加起)

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到了。

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

浙政钉2.0免登_第1张图片

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


浙政钉2.0免登_第2张图片

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


浙政钉2.0免登_第3张图片

初遇ZooKeeper


你可能感兴趣的:(java,jwt,接口,web,软件开发)