专有钉钉开放平台
浙政钉省级以及各地市应用技术支持群号
浙政钉省级以及各地市应用技术支持钉钉群号
省本级:群号:31431551
杭州:群号:34302232(1群) 35426643(2群)
嘉兴:群号:33291068
湖州:群号:35381358
绍兴:群号:34829275
金华:群号:35984633
宁波:群号:34049171
舟山:群号:34049228
温州:群号:31376536
台州:群号:34504434
衢州:群号:34286499
丽水:群号:33869997
按照官方文档中的教程配置
应用上架
第一步申请isv入驻
第二步用isv账号添加用户和机构
ps:专有钉钉下载地址
第三步用isv账号在团队管理授权普通用户管理员权限,在工作台中的权限管理中的权限角色管理授权开放平台管理员权限和用户工作台管理权限
ps:工作台管理在右上角ISV基本信息旁边
第四步在开发者后台-我的应用-组织内部应用中创建应用,创建成功后进入详情这里就鞥拿到ak和as
第五步应用配置如配置移动端地址,服务器出口之类的,这一步如移动端地址h5需要一个能连接外网的地址
ps:如果调用免登接口出现不在白名单情况,就将那个报错的ip加入
服务器出口ip
第六步应用权限配置,如果不知道建议先全部开启,知道的按需开启,应用访问范围也需要配置,如果应用需要跳转到其他应用需要允许跳转的第三方应用配置
第七步在应用发布中将应用上架
第八步进入工作台管理,这时候如果是普通用户那第三步的配置可能可能极为关键,点击用户工作台,新增一个用户工作台,之后配置页面应用,配置应用分组列表,将你的h5或app加入分组列表,在页面应用这个分组,之后发布工作站
第九步进入手机版工作台能看见配置好的应用就ok了
ps:如果看不到可能是应用配置的问题,可以去看一下控制台的应用管理看一下应用归属,登录手机的用户是否属于这个组织等等
这样应用是配置好了之后进行免登
免登
第十步 前端获取免登授权码
npm install gdt-jsapi
在页面引用,这段代码只要在专有钉钉下才生效,需要测试的话将res显示到页面之后部署到线上之后通过手机版专有钉钉访问才能拿到免登授权码;即第五步
import dd from 'gdt-jsapi';
dd.getAuthCode({}).then(res =>{
console.log(res)
}).
catch(err =>{})
第十一步后端调试免登
需要第四步中的as和ak
尝试获取access_token
ExecutableClient executableClient = ExecutableClient.getInstance();
executableClient.setAccessKey("ak");
executableClient.setSecretKey("as");
executableClient.setDomainName("openplatform.dg-work.cn");
executableClient.setProtocal("https");
executableClient.init();
//executableClient要单例,并且使用前要初始化,只需要初始化一次
String api = "/gettoken.json";
GetClient getClient = executableClient.newGetClient(api);
//设置参数
getClient.addParameter("appkey", "ak");
getClient.addParameter("appsecret", "as");
//调用API
String apiResult = getClient.get();
System.out.println(apiResult);
如果这一步正常则可以正式开始写代码
这里使用的是 spring colud 阿里系
配置nacos
dtalk:
appKey: xxxx
appSecret: xxxx
domainName: openplatform.dg-work.cn
如何使用查看动态获取nacos配置文件
添加数据库进行绑定
CREATE TABLE `user_dtalk` (
`id` varchar(30) NOT NULL,
`dtalk_id` varchar(128) DEFAULT NULL COMMENT '钉钉账号id',
`bind_user_id` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci 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 COMMENT '更新时间',
`bind_user_name` varchar(255) DEFAULT NULL COMMENT '系统用户名',
`authorization_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '登录信息',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
使用代码生成将数据库对应的java代码生成只展示主要代码
@Data
public class BindDataForm {
private String userName;
private String password;
private String authCode;
}
@Data
@Configuration
@RefreshScope
@Component
public class UserDtalkConfig {
@NacosValue(value = "${dtalk.domainName}", autoRefreshed = true)
public String domainName;
@NacosValue(value = "${dtalk.appKey}", autoRefreshed = true)
public String appKey;
@NacosValue(value = "${dtalk.appSecret}", autoRefreshed = true)
public String appSecret;
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisService {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param values 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> long setCacheSet(final String key, final Set<T> dataSet) {
Long count = redisTemplate.opsForSet().add(key, dataSet);
return count == null ? 0 : count;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
前面都是小条件其中 **R
**可以使用其他返回体代替
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/userdtalk")
@Api(value = "userdtalk", tags = "政务钉钉管理")
public class UserDtalkController {
private final UserDtalkService userDtalkService;
/**
* 浙政钉2.0
* @param bindDataForm
*/
@GetMapping("/loginAndBind")
public R loginAndBind(BindDataForm bindDataForm) {
return userDtalkService.doBindUser(bindDataForm);
}
}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.xxpt.gateway.shared.client.http.ExecutableClient;
import com.alibaba.xxpt.gateway.shared.client.http.PostClient;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class UserDtalkServiceImpl extends ServiceImpl<UserDtalkMapper, UserDtalk> implements UserDtalkService {
private final RedisService redisService;
private final UserDtalkConfig userDtalkConfig;
Logger log = LoggerFactory.getLogger(UserDtalkServiceImpl.class);
/**
* 政务端-浙政钉accessToken
*/
public final static String GOV_DTALK_ACCESS_TOKEN = "gov_dtalk_access_token";
public UserDtalkServiceImpl(RedisService redisService, UserDtalkConfig userDtalkConfig) {
this.redisService = redisService;
this.userDtalkConfig = userDtalkConfig;
}
public PostClient getClient(String api) {
ExecutableClient executableClient = ExecutableClient.getInstance();
executableClient.setAccessKey(userDtalkConfig.getAppKey());
executableClient.setSecretKey(userDtalkConfig.getAppSecret());
executableClient.setDomainName(userDtalkConfig.getDomainName());
executableClient.setProtocal("https");
executableClient.init();
return executableClient.newPostClient(api);
}
@Override
public String getAccessToken() {
//判断缓存中accessToken是否存在
String redisAccessToken = redisService.getCacheObject(GOV_DTALK_ACCESS_TOKEN);
if (StringUtils.isNotBlank(redisAccessToken)) {
return redisAccessToken;
}
try {
//缓存中不存在,则重新获取
String api = "/gettoken.json";
PostClient client = this.getClient(api);
client.addParameter("appkey", userDtalkConfig.getAppKey());
client.addParameter("appsecret", userDtalkConfig.getAppSecret());
//调用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");
redisService.setCacheObject(GOV_DTALK_ACCESS_TOKEN, accessToken, expiresIn, TimeUnit.SECONDS);
return accessToken;
}
} catch (Exception e) {
log.error("浙政钉-获取accessToken异常", e);
}
return null;
}
/**
* 根据authCode换取用户信息。也会去调用getAccessToken()方法拿Token。
*
* @param authCode
* @return
*/
@Override
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;
}
//当前方法一个用户可以绑定多个钉钉,自己可以改造
@Override
public R doBindUser(BindDataForm bindDataForm) {
JSONObject userData = this.getDingtalkAppUser(bindDataForm.getAuthCode());
if (userData == null) {
return R.failed("获取钉钉用户信息失败");
}
String accountId = userData.getString("accountId");
UserDtalk userDtalk = new UserDtalk();
userDtalk.setDtalkId(accountId);
userDtalk.setDtalkUserInfo(JSONObject.toJSONString(userData));
//判断是否绑定
LambdaQueryWrapper<UserDtalk> eq = new QueryWrapper<UserDtalk>()
.lambda().eq(UserDtalk::getDtalkId, accountId);
UserDtalk one = getOne(eq);
if (null == one) {
if (StringUtils.isEmpty(bindDataForm.getUserName()) || StringUtils.isEmpty(bindDataForm.getPassword())) {
return R.failed("暂未绑定用户,请先绑定");
}
//进行账号绑定
//调用登录接口使用账号密码之类
try {
Map login = login(bindDataForm.getUserName(), bindDataForm.getPassword());
//判断登录是否成功
if (!login.isEmpty()) {
//插入授权码,用户id,绑定信息
userDtalk.setBindUserId(login.get("user_id").toString());
userDtalk.setBindUserName(login.get("username").toString());
userDtalk.setAuthorizationCode(JSON.toJSONString(bindDataForm));
save(userDtalk);
return R.ok(login);
}
} catch (Exception e) {
log.error("浙政钉-绑定账户异常",e);
}
return R.failed("账号密码错误");
} else {
try {
//调用登录接口使用免登
BindDataForm bindDataForm1 = JSON.parseObject(one.getAuthorizationCode(), BindDataForm.class);
Map login = login(bindDataForm1.getUserName(), bindDataForm1.getPassword());
return R.ok(login);
} catch (Exception ignored) {
if (StringUtils.isEmpty(bindDataForm.getUserName()) || StringUtils.isEmpty(bindDataForm.getPassword())) {
return R.failed("绑定用户失效,请重新绑定");
}
try {
Map login = login(bindDataForm.getUserName(), bindDataForm.getPassword());
if (!login.isEmpty()) {
//插入授权码,用户id,绑定信息
one.setBindUserId(login.get("user_id").toString());
one.setBindUserName(login.get("username").toString());
one.setAuthorizationCode(JSON.toJSONString(bindDataForm));
updateById(one);
return R.ok(login);
}
} catch (Exception e) {
log.error("浙政钉-免密登录异常",e);
}
return R.failed("账号密码错误");
}
}
}
//这一步可以使用open接口直接获取内部接口
Map login(String username, String password) {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("isToken", "false");
requestHeaders.add("Authorization", "Basic YWRtaW46YWRtaW4=");
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
//登录参数由其他人提供
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://base-auth/auth/oauth/token")
.queryParam("username", username).queryParam("password", password)
.queryParam("grant_type", "password").queryParam("scope", "server")
.build();
URI uri = uriComponents.toUri();
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(params, requestHeaders);
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForObject(uri, httpEntity, Map.class);
}
}
免登就到此结束,可能之后会有扫码登录对接