Java Web SSO实现

项目简介

背景

该方案没有网络上通用sso中的认证中心

系统A、系统B已经运行多年,因政策要求,现要做一网通服务。系统A为全国系统,系统B为地方系统。用户登录系统B后,无需再次登录,即可通过系统B中的链接跳转至系统A。系统A和系统B有各自的用户数据

目前记录的是系统A为方案制定方,系统B按照系统A的方案进行改造。

思路

在这里插入图片描述

步骤简述:

  1. 用户登录系统B后点击办事事项链接(该链接指向系统B自定义的链接);
  2. 系统B自定义链接后台发送至 系统A生成令牌链接
    ?applicationUrl=系统B接收令牌链接&key=唯一码
    请求ssotoken。其中applicationUrl是用来接收令牌的地址(该地址应在系统A进行备案),key是系统B生成的唯一码;
  3. 系统A向applicationUrl推送令牌;
  4. 系统B使用令牌将系统A分配的系统密码进行加密,然后同系统A分配的系统ID、系统B已登录用户的账号一起发送给系统A的鉴权链接
  5. 系统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代码的顺序执行时不会改变的,因此,无需考虑线程暂停之类的问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值