一 需求
客户通过用友旗舰版公有云进入到旗舰版后,可跳转进入第三方系统,实现面免登。
二 需求分析
用户在进入用友旗舰版时已获得身份认证,如果实现第三方免登就是需要第三方系统提供免登策略,阿里邮箱提供免登策略为通过邮箱,由阿里邮箱返回该邮箱的免登地址在一分钟内有效。阿里不验证身份合法性,只要邮箱在当前的appid对应的企业中存在就会提供免登链接。
三 实现思路
阿里文档:
https://alidocs.dingtalk.com/i/p/nb9XJlNqOArrQGyA/docs/KOEmgBoGwD78vdPjbaY3VndLerP9b30a?dontjump=true
用友旗舰版文档:
https://c3.yonyoucloud.com/iuap-ipaas-base/ucf-wh/console-fe/open-home/index.html?locale=zh_CN#/doc-center/docDes/doc?code=open_jrwd&locale=zh_CN§ion=59cdfc00cb5f4f4eab00f633185f71d0
阿里免登是通过阿里邮箱换取免登连接:
整体步骤:
通过邮箱+accesstoken换取免密连接
外系统开发方案
旗舰版配置免登
免登地址配置
http://外系统免登中转地址?systype=aemail
首先获取旗舰版accesstoken
免登会在上述连接中携带code免登code
外系统获取code 向旗舰版code中发起验证申请,获得唯一yhtid
通过yhtid获取账号信息,拿到mail
获取阿里邮箱的accesstoken
获取免登连接
前台跳转阿里免登连接(1min内有效)
上述需求和方案分析后其实是一个应用集成需求,用友旗舰版为应用集成提供了免登能力,通过应用集成提供一个临时身份验证渠道,会在集成链接中传递免登code,免登code可调用旗舰版验证接口获得当前用户基本信息及员工信息,从中获取员工号,手机号,邮箱支撑调用第三方免登策略
四、简约dome
只有思路 可实现免登,不符合编码规范,java代码
package com.jjhg.gateway.controller;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import okhttp3.*;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Controller
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(LoggerFactory.class);
@Value( "${qjb.appKey}")
private String appKey;
@Value( "${qjb.appSecret}")
private String appSecret;
@Value( "${qjb.tokenUrl}")
private String tokenUrl;
@Value("${qjb.gatewayUrl}")
private String gatewayUrl;
@Value("${amail.aurl}")
private String aurl;
@Value("${amail.appkey}")
private String aappkey;
@Value( "${amail.secret}")
private String asecret;
private static final String HEADER_CONTENT_JSON = "application/json; charset=utf-8";
@RequestMapping(value = "/notPasswordlogin",method = RequestMethod.GET)
public void notPasswordlogin(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
String code = httpServletRequest.getParameter("code");
/**
* 调用旗舰版获取邮箱
*/
String email = getEmail(code);
log.info("【邮箱是】:{}",email);
String notPasswordUrl = getNotPasswordUrl(email);
log.info("【获取到阿里免密地址:】{}",notPasswordUrl);
httpServletResponse.sendRedirect(notPasswordUrl);
}
// 调用旗舰版 token
public String getBIPQJBtoken() throws Exception {
Map<String, String> params = new HashMap<>();
params.put("appKey", appKey);
String timestamp = String.valueOf(System.currentTimeMillis());
params.put("timestamp", timestamp);
String signature = this.HMACSHA256(params, appSecret);
StringBuilder url = new StringBuilder(tokenUrl + "/open-auth/selfAppAuth/getAccessToken");
url.append("?appKey=").append(appKey).append("×tamp=").append(timestamp).append("&signature=").append(signature);
log.info("access token url:{}", url);
// get请求
String result = doGetQjb(url.toString());
log.info("access token result:" + result);
JSONObject resultMap = com.alibaba.fastjson.JSON.parseObject(result);
return (String) resultMap.getJSONObject("data").get("access_token");
}
private String HMACSHA256(Map<String, String> params, String suiteSecret) throws Exception {
Map<String, String> treeMap;
if (params instanceof TreeMap) {
treeMap = params;
} else {
treeMap = new TreeMap<>(params);
}
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : treeMap.entrySet()) {
stringBuilder.append(entry.getKey()).append(entry.getValue());
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(suiteSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] signData = mac.doFinal(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
String base64String = Base64.getEncoder().encodeToString(signData);
return URLEncoder.encode(base64String, StandardCharsets.UTF_8);
}
// 获取邮箱
public String getEmail(String code) throws Exception {
log.info("免登录code:{}", code);
String accesstoken = getBIPQJBtoken();
// 换取userid
StringBuilder url1 = new StringBuilder(gatewayUrl + "/open-auth/selfAppAuth/getBaseInfoByCode");
url1.append("?access_token=").append(accesstoken).append("&code=").append(code);
StringBuilder url = new StringBuilder(gatewayUrl + "/yonbip/uspace/users/list_by_ids");
url.append("?access_token=").append(accesstoken);
String reponse = doGetQjb(url1.toString());
log.info("【换取id】:{}",reponse);
JSONObject reponseJsonOjbect = com.alibaba.fastjson.JSON.parseObject(reponse);
String yht_id = reponseJsonOjbect.getJSONObject("data").getString("yhtUserId");
// 兑换身份信息
HttpClient httpClient = new HttpClient();
PostMethod postMethod = new PostMethod(url.toString());
Map<String,Object> paramMap = new HashMap<>();
List<String> userIds = new LinkedList<>() {};
userIds.add(yht_id);
paramMap.put("userIds",userIds);
log.info("查询报文为userIds:{}", JSON.toJSONString(userIds));
log.info("查询报文为paramMap:{}", JSON.toJSONString(paramMap));
postMethod.setRequestHeader("Content-Type", HEADER_CONTENT_JSON);
postMethod.setRequestEntity(new StringRequestEntity( com.alibaba.fastjson.JSON.toJSONString(paramMap), "application/json", "UTF-8"));
String resultMsg = null;
try {
int status = httpClient.executeMethod(postMethod);
if (status != 200) {
log.error("请求地址:【" + url + "】失败,状态码为:【" + status + "】,错误消息为:【" + postMethod.getStatusText() + "】");
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(postMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
resultMsg = htmlRet.toString();
JSONObject resmap = com.alibaba.fastjson.JSON.parseObject(resultMsg);
String mail = resmap.getJSONArray("data").getJSONObject(0).getString("userEmail");
log.info("【邮箱是:】{}",mail);
return mail;
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
}
// 获取 阿里accesstoken
public String getAmailAccesstoken() throws UnsupportedEncodingException {
log.info("【阿里云accesstoken】");
HttpClient httpClient = new HttpClient();
StringBuilder url = new StringBuilder("https://alimail-cn.aliyuncs.com/oauth2/v2.0/token").append("?grant_type=client_credentials").append("&client_id=").append(aappkey).append("&client_secret=").append(asecret);
log.info("【阿里 accesstoken获取地址:】{}",url.toString());
PostMethod postMethod = new PostMethod(url.toString());
postMethod.setRequestHeader(new Header("content-type","application/x-www-form-urlencoded"));
// Map<String,Object> bodyMap = new HashMap<>();
// bodyMap.put("grant_type","client_credentials");
// bodyMap.put("client_id",aappkey);
// bodyMap.put("client_secret",asecret);
//
// postMethod.setRequestEntity(new StringRequestEntity( com.alibaba.fastjson.JSON.toJSONString(bodyMap), "application/json", "UTF-8"));
String resultMsg = null;
try {
int status = httpClient.executeMethod(postMethod);
if (status != 200) {
log.error("阿里accesstoken获取失败:状态码为:【" + status + "】,错误消息为:【" + postMethod.getStatusText() + "】");
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(postMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
log.info("请求成功");
String reqStr = htmlRet.toString();
log.info("阿里 accesstoken 响应结果: {}" ,reqStr);
JSONObject reqJsonObject = JSONObject.parseObject(reqStr);
String accessToken = reqJsonObject.getString("access_token");
log.info("【阿里accessToken:】:{}",accessToken);
return accessToken;
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
// request.body().toString();
// log.info("access token result:" + result);
// JSONObject resultMap = com.alibaba.fastjson.JSON.parseObject(result);
// return (String) resultMap.getJSONObject("data").get("access_token");
}
// 获取免登录地址
public String getNotPasswordUrl(String email) throws UnsupportedEncodingException {
HttpClient client = new HttpClient();
PostMethod postMethod = new PostMethod("https://alimail-cn.aliyuncs.com/v2/passwordless/link");
postMethod.setRequestHeader("Content-Type", HEADER_CONTENT_JSON);
postMethod.setRequestHeader("authorization", "bearer "+getAmailAccesstoken());
Map<String,String> bodyParam = new HashMap<>();
// bodyParam.put("email",email);
// 由于获取到的邮箱被加密了这里固定调用邮箱用于测试
bodyParam.put("email","xxx@xxx.cn");
if (bodyParam != null) {
postMethod.setRequestEntity(new StringRequestEntity( com.alibaba.fastjson.JSON.toJSONString(bodyParam), "application/json", "UTF-8"));
}
log.info("【开始获取阿里免登】 ");
String resultMsg = null;
try {
int status = client.executeMethod(postMethod);
if (status != 200) {
log.info("【阿里免登地址获取失败】:状态码{}",status);
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(postMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
log.info("请求成功 阿里免密地址:{}",htmlRet.toString());
String str = htmlRet.toString();
JSONObject resultMap = com.alibaba.fastjson.JSON.parseObject(str);
return (String) resultMap.getString("link");
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
}
/**
* post 请求
*
* @param url
* @param bodyParam
* @return
*/
public String doPostQjb(String url, Map<String, String> bodyParam) throws UnsupportedEncodingException {
HttpClient client = new HttpClient();
PostMethod postMethod = new PostMethod(url);
postMethod.setRequestHeader("Content-Type", HEADER_CONTENT_JSON);
if (bodyParam != null) {
postMethod.setRequestEntity(new StringRequestEntity( com.alibaba.fastjson.JSON.toJSONString(bodyParam), "application/json", "UTF-8"));
}
String resultMsg = null;
try {
int status = client.executeMethod(postMethod);
if (status != 200) {
log.error("请求地址:【" + url + "】失败,状态码为:【" + status + "】,错误消息为:【" + postMethod.getStatusText() + "】");
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(postMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
log.info("请求成功");
return htmlRet.toString();
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
}
/**
* get 请求
*
* @param url
* @return
*/
public String doGetQjb(String url) {
HttpClient client = new HttpClient();
GetMethod getMethod = new GetMethod(url);
System.out.println(url);
String resultMsg = null;
try {
int status = client.executeMethod(getMethod);
if (status != 200) {
log.error("请求地址:【" + url + "】失败,状态码为:【" + status + "】,错误消息为:【" + getMethod.getStatusText() + "】");
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(getMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
log.info("请求成功");
return htmlRet.toString();
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
}
/**
* get 请求
*
* @param url
* @return
*/
public static String doGetAEmail(String url) {
HttpClient client = new HttpClient();
GetMethod getMethod = new GetMethod(url);
String resultMsg = null;
try {
int status = client.executeMethod(getMethod);
if (status != 200) {
log.error("请求地址:【" + url + "】失败,状态码为:【" + status + "】,错误消息为:【" + getMethod.getStatusText() + "】");
return resultMsg;
} else {
BufferedReader reader = new BufferedReader(new InputStreamReader(getMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
String tmp = null;
StringBuffer htmlRet = new StringBuffer();
while ((tmp = reader.readLine()) != null) {
htmlRet.append(tmp);
}
log.info("请求成功");
return htmlRet.toString();
}
} catch (IOException e) {
log.error("请求网关失败" + e.getMessage(), e);
return resultMsg;
}
}
}
五 系统体系化编码建议
将用友旗舰版调用代码剥离出来形成iuapSdk提供旗舰版调用服务的能力,将阿里邮箱调用逻辑剥离出来提供阿里邮箱调用能力
多应用集成部署策略:
- 将各端服务能力形成service服务统一可抽象化Iservice服务接口通用调用逻辑
- 在入口端添加SysID信息采用策略模式调用被调用服务
- 持久化accessToken 参考开源项目weixin-java-tool,采用内存缓存或redis都可,因为减少请求频次(部分api会有访问频次限制)
- 对访问appid、Secret持久化存储
- 增加后台运维界面统计单点信息
六 后期计划
在时间充裕的情况下会实现五单独开源一个ation-iuap-sso,后续更新博客时使用吧,可关注我的码云仓库主页,初步构建选型为轻量级应用服务,采用spring-boot 3.x 采用java17实现,构建采用mybatis-plus,不依赖lombok实现,前端选型react+ant design+TypeScript(react不太会,如果学习难度太大可考虑使用vue3实现)实现,先实现总体框架和阿里云单点实现service服务,开源后支持贡献其他接口实现service,欢迎各位支持!~
https://gitee.com/zeyangwu
七 版本更新记录
2024-07-03 编写开发思路及实现demo