项目简介
背景
该方案没有网络上通用sso中的认证中心
系统A、系统B已经运行多年,因政策要求,现要做一网通服务。系统A为全国系统,系统B为地方系统。用户登录系统B后,无需再次登录,即可通过系统B中的链接跳转至系统A。系统A和系统B有各自的用户数据
目前记录的是系统A为方案制定方,系统B按照系统A的方案进行改造。
思路
步骤简述:
- 用户登录系统B后点击办事事项链接(该链接指向系统B自定义的链接);
- 系统B自定义链接后台发送至 系统A生成令牌链接
?applicationUrl=系统B接收令牌链接&key=唯一码 请求ssotoken。其中applicationUrl是用来接收令牌的地址(该地址应在系统A进行备案),key是系统B生成的唯一码; - 系统A向applicationUrl推送令牌;
- 系统B使用令牌将系统A分配的系统密码进行加密,然后同系统A分配的系统ID、系统B已登录用户的账号一起发送给系统A的鉴权链接
- 系统A校验信息是否为空,若不为空,则校验系统ID、系统密码是否正常;若正常则使用系统B已登录用户的账号查询用户在系统A的信息并进行登录,登录成功跳转至系统A办事页面,否则跳转至系统A登录失败页面
实现
代码主要是思路的体现,一些地方有省略。
系统B
模拟前端办事链接
<body>
<ul>
<li>
<button class="submit" type="submit" id="submit"
onclick="javascrtpt:doJump()">某某办事事项</button>
</li>
</ul>
</body>
<script type="text/javascript">
function doJump() {
window.location.href = "http://localhost:8080/JavaSsoWebDemo/SsoServlet";
}
</script>
SsoServlet
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
/**
*
* @Description: 请求获取ssotoken的servlet
* @Team: ufgov.com
* @Author: lihhz
* @Date: 2018年10月22日
*/
public class SsoServlet extends HttpServlet {
private static final long serialVersionUID = -6187964647473231392L;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");
// 系统A分配的应用ID
String proId = "001";
// 项目Secret(公共应用Secret),由系统A分配的应用Secret
String proPass = "1233";
// 这里模拟的是生成一个唯一key,用来保存在/ReceiveServlet中返回的ssotoken
String uuid = UUID.randomUUID().toString();
boolean f = sendGET(
"系统A生成令牌链接
?applicationUrl=系统B接收令牌链接&key=唯一码"
+ uuid);
if (f) {
System.out.println("发送成功!");
}
synchronized (DataCenter.TOKEN_MAP) {
// 获取在/ReceiveServlet中返回的ssotoken
String ssotoken = DataCenter.TOKEN_MAP.get(uuid);
// 移除key
DataCenter.TOKEN_MAP.remove(uuid);
// 注意:这里进行加密,具体的加密算法这里不展开,只要和系统A加密约定即可
......省略加密过程
// 系统B已登录用户的账号,这里只是模拟而已
String info = "账号",userName="用户名称",type="2";
// 输出一个自动提交的表单。可以采用其他方法
PrintWriter out = response.getWriter();
out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
out.println("<head>");
out.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");
out.println("<title>跳转。。。</title>");
out.println("</head>");
out.println("<body onload=\"document.test.submit()\">");
out.println("<form name=\"test\" action=\"系统A鉴权路径\" method=\"POST\" style=\"display: none\">");
out.println("<input name=\"proId\" value=\"" + proId + "\"></input>");
out.println("<input name=\"proPass\" value=\"" + signature + "\"></input>");
out.println("<input name=\"info\" value=\"" + info + "\"></input>");
out.println("<input name=\"userName\" value=\"" + userName + "\"></input>");
out.println("<input name=\"type\" value=\"" + type + "\"></input>");
out.println("</form>");
out.println("正在跳转。。。");
out.println("</body>");
out.println("</html>");
out.flush();
out.close();
}
}
/***
* 向指定URL发送GET方法的请求
*
* @param apiUrl
* @param data
* @param headers
* @param encoding
* @return
*/
public boolean sendGET(String apiUrl) {
System.out.println("apiUrl = "+apiUrl);
try {
// 建立连接
URL url = new URL(apiUrl);
/* 获取客户端向服务器端传送数据所依据的协议名称 */
......如果需要处理协议,这里处理
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 需要输出
httpURLConnection.setDoOutput(true);
// 需要输入
httpURLConnection.setDoInput(true);
// 不允许缓存
httpURLConnection.setUseCaches(false);
httpURLConnection.setRequestMethod("GET");
// 连接会话
httpURLConnection.connect();
// if (data != null) {
// // 建立输入流,向指向的URL传入参数
// DataOutputStream dos = new DataOutputStream(httpURLConnection.getOutputStream());
// // 设置请求参数
// dos.write(data.getBytes("UTF-8"));
// dos.flush();
// dos.close();
// }
// 获得响应状态
int http_StatusCode = httpURLConnection.getResponseCode();
String http_ResponseMessage = httpURLConnection.getResponseMessage();
StringBuffer strBuffer;
if (HttpURLConnection.HTTP_OK == http_StatusCode) {
System.out.println("发送成功");
strBuffer = new StringBuffer();
String readLine = new String();
BufferedReader responseReader = new BufferedReader(
new InputStreamReader(httpURLConnection.getInputStream(), "UTF-8"));
while ((readLine = responseReader.readLine()) != null) {
strBuffer.append(readLine);
}
responseReader.close();
String result = strBuffer.toString();
if (null == result || result.length() == 0) {
// 断开连接
httpURLConnection.disconnect();
return true;
}
JSONObject jsonObj = JSONObject.fromObject(result);
Object error = jsonObj.get("error");
if(error != null ){
System.out.println(error.toString());
return false;
}
return false;
} else {
throw new Exception(
MessageFormat.format("请求失败,失败原因: Http状态码 = {0} , {1}", http_StatusCode, http_ResponseMessage));
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
ReceiveServlet
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
/***
*
* @Description: 接收ssotoken的servlet
* @Team: ufgov.com
* @Author: lihhz
* @Date: 2018年10月22日
*/
public class ReceiveServlet extends HttpServlet {
private static final long serialVersionUID = -6187964647473231392L;
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取post参数
StringBuffer sb = new StringBuffer();
InputStream is = request.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String s = "";
while ((s = br.readLine()) != null) {
sb.append(s);
}
String str = sb.toString();
JSONObject obj = JSONObject.fromObject(str);
//查看是否有错误信息
if(obj.containsKey("error")){
System.out.println(obj.getString("error"));
return;
}
// 系统A生成的令牌
String ssotoken = obj.getString("ssotoken");
// 财政对接系统生成的key,有财政部系统转发过来
String key = obj.getString("key");
synchronized (DataCenter.TOKEN_MAP) {
DataCenter.TOKEN_MAP.put(key, ssotoken);
}
}
}
DataCenter
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class DataCenter {
/**
* 用来存储key及令牌
*/
public static final ConcurrentMap<String, String> TOKEN_MAP = new ConcurrentHashMap<String, String>();
}
系统A
//获取令牌
@RequestMapping(value = "/token", method = RequestMethod.GET)
@ResponseBody
public void token(HttpServletRequest request) {
try {
String key = request.getParameter("key");
String applicationUrl = request.getParameter("applicationUrl");
LOGGER.info("key=" + key + ",applicationUrl=" + applicationUrl);
......信息校验:是否为空、是否在配置表存在等。这里略过
JSONObject jsonObject = new JSONObject();
jsonObject.put("key", key);
if(error != null){
jsonObject.put("error", error);
}else {
......生成ssotoken,1分钟有效期,注意及时清除。略
jsonObject.put("ssotoken", ssotoken);
}
HttpUtil.sendMsg2(jsonObject, applicationUrl);
}catch (IOException e){
LOGGER.error(e);
e.printStackTrace();
}
}
/**
* 鉴权。包括系统信息及账号信息
*/
@RequestMapping(value = "/auth", method = RequestMethod.POST)
public void sysAuth(HttpServletRequest request, HttpServletResponse response) {
try {
// 跳转Url
String redirectUrl = "/api/ssofail?msg=";
Object proId = request.getParameter("proId"),
proPass = request.getParameter("proPass"),
info = request.getParameter("info"),
userName = request.getParameter("userName"),
type = request.getParameter("type");
String msg = null;
......基本信息鉴权:为空、在库中是否存在。略
......如果不存在,在重定向至系统A自定义的错误页面
......查询登录用户在系统A中的信息,不存在则重定向至错误页面
......令牌鉴定、系统id、密码鉴定,不正确则重定向至错误页面
if (proId.toString().equals(sysSsoInfo.getproId())) {
......模拟登录
} else {
redirectUrl = "/api/ssofail";
}
response.sendRedirect(redirectUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
总结
在设计和实现过程中,最大的实现难点,居然是在http通信的时候遇到的。首先,重定向只能发生在浏览器中,也就是浏览器发起请求,那么对应的会有一个request和response,重定向只能发生在这个response中。然后,不过http的通信有多么复杂,Java代码的顺序执行时不会改变的,因此,无需考虑线程暂停之类的问题了。