写在前面:
目前主要做的是政府一块类的项目,其中大大小小项目免不了通过浙政钉来访问进而可以实现免登操作。其实看了之后还蛮简单的,最近项目中又用到了,趁此机会打卡记录学习一下。为啥是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<UserDtalk> {
-
/**
-
* 政务钉钉-获取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<String, String> 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<User> 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<UserDtalk>().e
q("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到了。
想看前面几期文章 请点击下列图片
转载:https://blog.csdn.net/Boom_404/article/details/114959157