钉钉微应用免密登录前后端流程
钉钉免密登录介绍
钉钉免密登录是指用户在钉钉内部打开应用时,无需输入用户名和密码,可以直接获取当前钉钉信息进行登录。这种登录方式可以大大简化用户的登录流程,提高登录效率。
免密登录的实现方式有多种,其中一种是在钉钉内部应用中调用钉钉提供的接口,获取用户的身份信息,从而实现免密登录。另外,对于一些第三方应用,可以通过集成钉钉的SDK或者使用钉钉提供的OAuth2.0授权登录方式来实现免密登录。
免密登录的优点包括:
(1)简化登录流程:用户无需输入用户名和密码,直接通过钉钉信息进行登录,大大简化了登录流程。
(2)提高安全性:由于不需要用户输入密码,因此可以避免密码被盗取或泄露的风险。
(3)提高效率:用户无需等待输入密码,可以快速地进入应用,提高了使用效率。
需要注意的是,免密登录并不适用于所有场景,对于一些需要更高安全性的应用,仍然需要用户进行密码验证或者使用其他安全措施来保证登录的安全性。
准备工作
在前端方面使用Vue来实现钉钉免密登录,通常涉及到与后端服务的交互以及调用钉钉提供的接口。在开始实现免密登录前,一是确保你的Vue项目已经设置好,并且可以运行;二是确保你已经有了后端服务,以便与Vue应用进行通信。
钉钉开发者后台的员工权限记得打开,根据实际需要设置开启范围和权限范围
实现结果
在工作台中找到自己的微应用小程序
可以在vConsole中看到后端传递的结果
这里是我做的小demo,最终的实现效果如下,当点击新增需求时,填写新工单时,例如姓名,部门,手机号等信息是通过钉钉免密登录获取到的用户信息。
前端方面(vue+axios)
在登录页中如下方法:
import { getddCode } from "../utils/ddtalk";
import { getdd } from "../api/login";
......
created() {
// 调用方法
this.getUserList();
},
methods: {
getUserList() {
var that = this;
//getCode是公共js的方法
getddCode((code) => {
//code是公共方法通过callback返回的
var ddCode = code;
getdd(ddCode).then((res) => {
if(res.data.code == 200){
// 获取用户信息成功,将用户信息保存到vuex中 || 直接保存到本页面的data中
that.$store.commit('ASYNCGETUSERINFO',res.data.result)
}
});
});
},
},
在utils下创建ddtalk.js
ddtalk.js代码如下
import * as dd from "dingtalk-jsapi";
export function getddCode(callback) {
// 企业id 在钉钉开发者后台可以查看
let corpId = "xxxxxxxxxxxxxxxxx";
if (dd.env.platform !== "notInDingTalk") {
dd.ready(() => {
//使用SDK 获取免登授权码
dd.runtime.permission.requestAuthCode({
corpId: corpId,
onSuccess: (res) => {
callback(res.code);
},
onFail: (err) => {
//报错以数组格式返回
alert("mqp" + JSON.stringify(err));
},
});
});
//验证失败
dd.error(function(err) {
alert("进入到error中");
alert(JSON.stringify(err));
});
}
}
在api下创建login.js
login.js代码如下
import request from '../utils/request'
// 钉钉token 回调
export function getdd(code) {
return request({
url: `/dd/login/${code}`,
method: 'get',
})
}
后端方面(Springboot)
创建DdController
package com.shwx.callit.api.controller.v1;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.exceptions.ApiException;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiV2UserGetRequest;
import com.dingtalk.api.request.OapiV2UserGetuserinfoRequest;
import com.dingtalk.api.response.OapiV2UserGetResponse;
import com.dingtalk.api.response.OapiV2UserGetuserinfoResponse;
import com.shwx.callit.constant.utils.DingDingSample;
import com.shwx.callit.constant.utils.Result;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/dd")
@CrossOrigin("*")
@Api(value = "/dd", tags = "")
public class DdController {
@GetMapping("/login/{code}")
public Result<?> getCode(@PathVariable("code") String code) throws Exception {
/**
* 使用AppKey 和AppSecret 获取accessToken
*/
String appkey = "xxxxxxxxxxxxxxxxxxx"; // 在钉钉开发者后台可以查看
String appsecret = "xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxx"; // 在钉钉开发者后台可以查看
String accessToken = DingDingSample.getToken(appkey,appsecret);
// 获取用户id
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo");
OapiV2UserGetuserinfoRequest req = new OapiV2UserGetuserinfoRequest();
req.setCode(code);
OapiV2UserGetuserinfoResponse rsp = client.execute(req, accessToken);
if (rsp != null && rsp.getErrcode() == 0){
JSONObject rspJson = JSONObject.parseObject(rsp.getBody());
String result = rspJson.getString("result");
JSONObject rspResult = JSONObject.parseObject(result);
String userid = rspResult.getString("userid");
// 获取用户详情信息
OapiV2UserGetResponse userRsp = getUserInfo(accessToken,userid);
if (userRsp != null && userRsp.getErrcode() == 0){
// 返回的类型根据实际需求自己定义,我这里返回Json格式,因此需要将String类型转为json格式
JSONObject userJson = JSONObject.parseObject(userRsp.getBody());
String userRes = userJson.getString("result");
JSONObject userInfo = JSONObject.parseObject(userRes);
// 将结果返回给前端
return Result.oK(userInfo);
}
return Result.error("获取用户信息失败!");
}
return Result.error("系统异常!");
}
public OapiV2UserGetResponse getUserInfo(String accessToken,String userid){
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/get");
OapiV2UserGetRequest req = new OapiV2UserGetRequest();
req.setUserid(userid);
req.setLanguage("zh_CN");
return client.execute(req, accessToken);
} catch (ApiException | com.taobao.api.ApiException e) {
e.printStackTrace();
}
return null;
}
}
在urils下创建HttpClientUtils
package com.shwx.callit.constant.utils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* 依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
*/
public class HttpClientUtils {
public static final int connTimeout = 10000;
public static final int readTimeout = 10000;
public static final String charset = "UTF-8";
private static HttpClient client = null;
static {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(128);
cm.setDefaultMaxPerRoute(128);
client = HttpClients.custom().setConnectionManager(cm).build();
}
public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception {
return post(url, parameterStr, "application/x-www-form-urlencoded", charset, connTimeout, readTimeout);
}
public static String postParameters(String url, String parameterStr, String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception {
return post(url, parameterStr, "application/x-www-form-urlencoded", charset, connTimeout, readTimeout);
}
public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String postParameters(String url, Map<String, String> params, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String get(String url) throws Exception {
return get(url, charset, null, null);
}
public static String get(String url, String charset) throws Exception {
return get(url, charset, connTimeout, readTimeout);
}
/**
* 发送一个 Post 请求, 使用指定的字符集编码.
*
* @param url
* @param body RequestBody
* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
* @param charset 编码
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return ResponseBody, 使用指定的字符集编码.
* @throws ConnectTimeoutException 建立链接超时异常
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String post(String url, String body, String mimeType, String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
String result = "";
try {
if (StringUtils.isNotBlank(body)) {
HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
post.setEntity(entity);
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 提交form表单
*
* @param url
* @param params
* @param connTimeout
* @param readTimeout
* @return
* @throws ConnectTimeoutException
* @throws SocketTimeoutException
* @throws Exception
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
try {
if (params != null && !params.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
Set<Entry<String, String>> entrySet = params.entrySet();
for (Entry<String, String> entry : entrySet) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
post.setEntity(entity);
}
if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
}
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null
&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
}
/**
* 发送一个 GET 请求
*
* @param url
* @param charset
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return
* @throws ConnectTimeoutException 建立链接超时
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String get(String url, String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpGet get = new HttpGet(url);
String result = "";
try {
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
get.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(get);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(get);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
get.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 从 response 里获取 charset
*
* @param ressponse
* @return
*/
@SuppressWarnings("unused")
private static String getCharsetFromResponse(HttpResponse ressponse) {
// Content-Type:text/html; charset=GBK
if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
String contentType = ressponse.getEntity().getContentType().getValue();
if (contentType.contains("charset=")) {
return contentType.substring(contentType.indexOf("charset=") + 8);
}
}
return null;
}
/**
* 创建 SSL连接
*
* @return
* @throws GeneralSecurityException
*/
private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
@Override
public void verify(String host, SSLSocket ssl)
throws IOException {
}
@Override
public void verify(String host, X509Certificate cert)
throws SSLException {
}
@Override
public void verify(String host, String[] cns,
String[] subjectAlts) throws SSLException {
}
});
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (GeneralSecurityException e) {
throw e;
}
}
}