SpringBoot 简单实现仿CAS单点登录系统

新境界开源开源SSO项目介绍

最近看了一下杰哥开源的项目 https://gitee.com/zhanghejie/jeexjj_sso 感觉写的不错.

自己根据项目的思路简单写了一个demo。在介绍demo之前我先简单介绍一下杰哥sso项目的思路。

新境界开源SSO项目实现原理大致如下:

首先要说明的是杰哥 开源 sso项目 jeexjj_sso 是基于 cookie 实现的, 通过 GrantingTicketServiceTicket 来完成一系列的登录操作。

首先说明一下 GrantingTicketServiceTicket 是什么?

  1. GrantingTicket: 来表示用户登录的唯一标识票据。
  2. ServiceTicket:来表示每个 sso客户端应用的信息。如果一个用户登陆了基于sso的三个互相信任的系统。那么对应该用户总共有1个GrantingTicket,3个ServiceTicket

新境界开源SSO项目登录流程介绍

登陆窗口在client端的登陆流程如下:

/zhanghejie/jeexjj_sso/raw/master/doc/img/xulietu1.png

新境界开源SSO项目授权登录流程介绍

已经登陆sso的用户首次访问某信任系统时的验证流程
在这里插入图片描述

新境界开源SSO项目退出流程介绍

单点退出流程
在这里插入图片描述

自己写单点登录Demo

单点登录Demo介绍

为了练手自己尝试写了一个Demo,但是实现思路和杰哥的 sso项目大体是一致的。涉及到sso的单点登录一般需要处理3块业务逻辑:

  1. 单点登录
    就是通过sso服务端进行登录的操作。
  2. 单点授权验证
    当在sso服务登录成功后访问别的项目无需在次登录的操作。
  3. 单点退出
    当一个sso客户端退出后其他的sso客户端也需要统一退出的操作。

单点登录

大致原理如下:

  1. 我们sso客户端访问需要登录的url 或者登录的url 直接跳转到sso服务端的登录页面(如果已经登录则直接跳转到访问的url)

    1. 直接访问登录的url,sso客户端的拦截器会将其直接重定向到 sso服务端的登录页面并携带sso客户端访问地址。
    2. 访问sso客户端需要登录的url(没有登录的情况下)会重定向到sso服务端单点授权验证逻辑中。如果该用户未登录则重定向到 sso服务端的登录页面并携带sso客户端访问地址。
  2. 输入用户名和密码在sso服务端进行登录,如果用户名和密码错误则重新跳转到登陆页面并提示错误信息。

  3. 如果用户名和密码正确,则生成 GrantingTicket 并将GrantingTicket 的id 放入cookie中。然后生成 ServiceTicket 并将ServiceTicket 放入GrantingTicket 的缓存map中。

  4. 将生成 GrantingTicket放入到缓存中(key:GrantingTicket的id value:GrantingTicket)然后再将ServiceTicket 的id 和对应的GrantingTicket的id放入缓存中。

  5. 重定向到sso客户端 并将 ServiceTicket 的id(ticket)拼接到重新向的url中。

  6. sso客户端根据ticket 再次调用sso服务端去获取用户信息。

  7. sso服务端接受到sso客户端 传来ticket后, 根据ticket 获取缓存中的 GrantingTicket的 id,然后再根据GrantingTicket的id 获取GrantingTicket。然后再从GrantingTicket获取用户信息返回给 sso客户端。
    8.sso客户端接受到用户信息后将其保存在session中。然后再将ticket 和session的关系存入sso客户端 缓存的map中。

  8. 如果sso客户端根据ticket 获取用户失败则返回sso服务端的登录页面并提示非法操作。

单点授权验证

大致原理如下:

  1. 当我们访问需要登录url时并且用户session信息为null的情况下 则走单点授权验证。
  2. 如果该用户未登录则重定向到 sso服务端的登录页面并携带sso客户端访问地址。后面的就走sso单点登录操作。
  3. 如果该用户已经在别的系统已经登录过。sso客户端拦截器拦截到该操作是单点授权验证后会重定向到sso服务端单点授权url。
    4.sso服务端拦截器拦截到 单点授权url后 首先获取 cookie中 GrantingTicket的id 。然后根据GrantingTicket的id获取缓存中的GrantingTicket。
  4. sso服务端 生成ServiceTicket 并放入到GrantingTicket的缓存中。
  5. sso服务端 重定向到sso客户端并携带ServiceTicket 的id (ticket)。
  6. sso客户端拦截器拦截到重定向url并获取到ticket 。然后根据ticket从sso服务端获取用户的信息。
  7. sso服务端接受到sso客户端ticket 根据ticket 获取缓存中的 GrantingTicket的id,然后再根据GrantingTicket的id 获取GrantingTicket。然后再从GrantingTicket获取用户信息,返回给客户端。
  8. sso客户端接受到用户的信息后 将信息保存在session中
  9. 然后将 ticket 和sesion的关系对应保存在缓存中。

单点退出

  1. sso客户端点击退出按钮,会向sso服务端发起退出登录操作。
  2. sso服务端拦截器拦截到退出的操作后会将进入退出的操作逻辑
  3. sso服务端 第一步先获取sso服务端在存储的GrantingTicket的cookie信息
  4. 根据在cookie中存储的GrantingTicket的id 从缓存中获取到GrantingTicket。
  5. 然后将sso服务端在存储的GrantingTicket的cookie信息清除。
  6. 获取GrantingTicket 缓存的ServiceTicket缓存map信息。
  7. 遍历ServiceTicket的Map信息。
  8. 通过ServiceTicket的host信息依次调用客户端的退出url并携带ticket。
  9. sso客户端拦截器拦截到退出的操作并获取到ticket.根据之前在客户端缓存的map中根据ticket获取到sesion的信息并调用sess.invalidate();
  10. sso服务端重定向到sso服务端的登录页面。

sso客户端拦截器代码:

public class SSOClientFilter implements Filter{
	private final String SUCESSURL = "/index/index";
	private final String SSOSERVERLOGINURL = "http://localhost:8080/ssoServer/login/login";
	private final String SSOSERVERAUTHENTICATIONURL = "http://localhost:8080/ssoServer/sso/authentication";
	public static ConcurrentHashMap<String,HttpSession> TICKET_SESSION_CACHE = new ConcurrentHashMap<String,HttpSession>();
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
			FilterChain filterChain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest )servletRequest;
		HttpServletResponse response = (HttpServletResponse )servletResponse;
		HttpSession session = request.getSession();
		String path = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
		String uri = request.getRequestURI();
		
		//被动单点退出
		String requestLogoutTicket = request.getParameter("requestLogout");
		if(!StringUtils.isEmpty(requestLogoutTicket)){
			ssoLogout(requestLogoutTicket);
			response.sendRedirect(SSOSERVERLOGINURL+"?service="+path);
			return;
		}
		//根据ticket获取用户信息
		String ticket = request.getParameter("ticket");
		if(!StringUtils.isEmpty(ticket)){
			User user = getUserByTicket(ticket);
			if(user == null){
				response.sendRedirect(SSOSERVERLOGINURL+"ERRORMSG="+"非法访问");
				return;
			}else{
				session.setAttribute("LOGIN_INFO", user);
				response.sendRedirect(path+SUCESSURL);
				TICKET_SESSION_CACHE.put(ticket, session);
				return;
			}
			
		}
		
		User user = (User)session.getAttribute("LOGIN_INFO");
		if(uri.endsWith("/login")){
			response.sendRedirect(SSOSERVERLOGINURL+"?service="+path);
			return;
		}
		if(null != user || uri.endsWith("css") || uri.endsWith("js")){
			filterChain.doFilter(servletRequest, servletResponse);
			return;
		}else{
			response.sendRedirect(SSOSERVERAUTHENTICATIONURL+"?service="+path);
			return;
		}
	}
	
	private void ssoLogout(String ticket) {
		if(TICKET_SESSION_CACHE.containsKey(ticket)){
			HttpSession sess = TICKET_SESSION_CACHE.get(ticket);
			if(null != sess){
				sess.invalidate();
			}
			TICKET_SESSION_CACHE.remove(ticket);
		}
	}
	private User getUserByTicket(String ticket)  {

		User user = null;
		CloseableHttpResponse closeableHttpResponse = null;
		try {
			CloseableHttpClient httpClient = HttpClients.createDefault();
			URI uriHttp = uriHttp = new URI("http://localhost:8080/ssoServer/sso/validateTicket");
			URIBuilder uriBuilder = new URIBuilder(uriHttp);
			uriBuilder.setParameter("ticket", ticket);
			URI uriParma  = uriBuilder.build();
			HttpGet httpGet = new HttpGet(uriParma);
			//执行请求访问
			closeableHttpResponse = httpClient.execute(httpGet);
			//获取返回HTTP状态码
			int satausCode = closeableHttpResponse.getStatusLine().getStatusCode();
			if(satausCode == 200 ){
				HttpEntity entity = closeableHttpResponse.getEntity();
				String content = EntityUtils.toString(entity,"UTF-8");
				user = JSONUtil.toBean(content, User.class);
				EntityUtils.consume(entity);
			}
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (URISyntaxException e) {
			e.printStackTrace();
		}finally{
			try {
				closeableHttpResponse.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return user;
	}
}

sso服务端拦截器代码:

public class SSOServerFilter implements Filter{
	private final String LOGINURL = "http://localhost:8080/ssoServer/login/login";
	private final String cookieName = "zhuoqianmingyue";
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
			FilterChain filterChain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest )servletRequest;
		HttpServletResponse response = (HttpServletResponse )servletResponse;
		
		String uri = request.getRequestURI();
		
		if(uri.endsWith("sso/validateTicket")){
			String ticket = request.getParameter("ticket");
			String service = request.getParameter("service");
			
			String gtId = TicketCache.getGTByST(ticket);
			GrantingTicket grantingTicket = TicketCache.getGrantingTicket(gtId);
			User user = grantingTicket.getUser();
			String jsonStr = JSONUtil.toJsonStr(user);
			
			//让ticket过期
			ServiceTicket serviceTicket = grantingTicket.getServiceTicketMap().get(ticket);
			boolean expired = serviceTicket.isExpired();
			if(expired){
				serviceTicket.setExpired(false);
				response.getOutputStream().write(jsonStr.getBytes("UTF-8"));
				return;
			}else{
				response.sendRedirect(LOGINURL+"?srevice="+service+"&ERRORMSG=ticket已经过期!");
				return;
			}
			
			
		}else if(uri.endsWith("sso/signin")){
			String loginName = request.getParameter("loginName");
			String password = request.getParameter("password");
			String service = request.getParameter("service");
			if("lijunkui".equals(loginName)){
				UUID randomUUID = UUID.randomUUID();
				String ticketId = randomUUID.toString();
				GrantingTicket gt = new GrantingTicket();
				gt.setId(ticketId);
				User user = new User();
				user.setUserId(1l);
				user.setLoginName("lijunkui");
				user.setPassword("123456");
				gt.setUser(user);
				
				Cookie cookie = new Cookie("zhuoqianmingyue", ticketId);		
				cookie.setMaxAge(-1);	
				cookie.setPath("/");
				response.addCookie(cookie);
				TicketCache.putGrantingTicket(ticketId, gt);
				
				ServiceTicket st = new ServiceTicket();
				String stId = UUID.randomUUID().toString();
				st.setId(stId);
				st.setHost(service);
				ConcurrentHashMap<String, ServiceTicket> serviceTicketMap = gt.getServiceTicketMap();
				serviceTicketMap.put(stId, st);
				TicketCache.putSTAndGT(stId, ticketId);
				response.sendRedirect(service+"?ticket="+st.getId());
				return;
			}else{
				response.sendRedirect(LOGINURL+"?srevice="+service+"&ERRORMSG=登录名或者密码错误");
				return;
			}
		}else if(uri.endsWith("sso/authentication")){
			String gtId = getSSOCookie(request,response);
			String service =  request.getParameter("service");
			GrantingTicket gt = null;
			if(gtId != null){
				gt = TicketCache.getGrantingTicket(gtId);
			}
		
			if(StringUtils.isEmpty(service) || null == gtId || null == gt){
				response.sendRedirect(LOGINURL+"?srevice="+service);
				return;
			}
			
			ServiceTicket st = new ServiceTicket();
			String stId = UUID.randomUUID().toString();
			st.setId(stId);
			st.setHost(service);
			ConcurrentHashMap<String, ServiceTicket> serviceTicketMap = gt.getServiceTicketMap();
			serviceTicketMap.put(stId, st);
			TicketCache.putSTAndGT(stId, gt.getId());
			response.sendRedirect(service+"?ticket="+st.getId());
			return;
		}else if(uri.endsWith("sso/signout")){
			String gtCookie = getSSOCookie(request, response);
			String service = request.getParameter("service");
			ssoLogout(request,response,gtCookie);
			response.sendRedirect(LOGINURL+"?service="+service);
			return;
		}
		
		filterChain.doFilter(servletRequest, servletResponse);	
	}

测试步骤:

  1. 点单登录Demo并没有对接mysql数据库 在进行部署测试的时候请使用 登录名:lijunkui 密码:123456 进行登录
  2. 访问 http://localhost:8090/ssoClient/index/index 或者
    http://localhost:8090/ssoClient/login 进行登录
  3. 登录成功后会跳转到 http://localhost:8090/ssoClient/index/index。
  4. 然后再访问 http://localhost:8070/ssoClient2/index/index 进行测试是时候可以直接登录。

示例程序代码地址:https://gitee.com/junkuiLee/ssoexamples

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值