SpringBoot 简单实现仿CAS单点登录系统
新境界开源开源SSO项目介绍
最近看了一下杰哥开源的项目 https://gitee.com/zhanghejie/jeexjj_sso 感觉写的不错.
自己根据项目的思路简单写了一个demo。在介绍demo之前我先简单介绍一下杰哥sso项目的思路。
新境界开源SSO项目实现原理大致如下:
首先要说明的是杰哥 开源 sso项目 jeexjj_sso 是基于 cookie 实现的, 通过 GrantingTicket 和 ServiceTicket 来完成一系列的登录操作。
首先说明一下 GrantingTicket 和 ServiceTicket 是什么?
- GrantingTicket: 来表示用户登录的唯一标识票据。
- ServiceTicket:来表示每个 sso客户端应用的信息。如果一个用户登陆了基于sso的三个互相信任的系统。那么对应该用户总共有1个GrantingTicket,3个ServiceTicket。
新境界开源SSO项目登录流程介绍
登陆窗口在client端的登陆流程如下:
新境界开源SSO项目授权登录流程介绍
已经登陆sso的用户首次访问某信任系统时的验证流程
新境界开源SSO项目退出流程介绍
单点退出流程
自己写单点登录Demo
单点登录Demo介绍
为了练手自己尝试写了一个Demo,但是实现思路和杰哥的 sso项目大体是一致的。涉及到sso的单点登录一般需要处理3块业务逻辑:
- 单点登录
就是通过sso服务端进行登录的操作。 - 单点授权验证
当在sso服务登录成功后访问别的项目无需在次登录的操作。 - 单点退出
当一个sso客户端退出后其他的sso客户端也需要统一退出的操作。
单点登录
大致原理如下:
-
我们sso客户端访问需要登录的url 或者登录的url 直接跳转到sso服务端的登录页面(如果已经登录则直接跳转到访问的url)
- 直接访问登录的url,sso客户端的拦截器会将其直接重定向到 sso服务端的登录页面并携带sso客户端访问地址。
- 访问sso客户端需要登录的url(没有登录的情况下)会重定向到sso服务端单点授权验证逻辑中。如果该用户未登录则重定向到 sso服务端的登录页面并携带sso客户端访问地址。
-
输入用户名和密码在sso服务端进行登录,如果用户名和密码错误则重新跳转到登陆页面并提示错误信息。
-
如果用户名和密码正确,则生成 GrantingTicket 并将GrantingTicket 的id 放入cookie中。然后生成 ServiceTicket 并将ServiceTicket 放入GrantingTicket 的缓存map中。
-
将生成 GrantingTicket放入到缓存中(key:GrantingTicket的id value:GrantingTicket)然后再将ServiceTicket 的id 和对应的GrantingTicket的id放入缓存中。
-
重定向到sso客户端 并将 ServiceTicket 的id(ticket)拼接到重新向的url中。
-
sso客户端根据ticket 再次调用sso服务端去获取用户信息。
-
sso服务端接受到sso客户端 传来ticket后, 根据ticket 获取缓存中的 GrantingTicket的 id,然后再根据GrantingTicket的id 获取GrantingTicket。然后再从GrantingTicket获取用户信息返回给 sso客户端。
8.sso客户端接受到用户信息后将其保存在session中。然后再将ticket 和session的关系存入sso客户端 缓存的map中。 -
如果sso客户端根据ticket 获取用户失败则返回sso服务端的登录页面并提示非法操作。
单点授权验证
大致原理如下:
- 当我们访问需要登录url时并且用户session信息为null的情况下 则走单点授权验证。
- 如果该用户未登录则重定向到 sso服务端的登录页面并携带sso客户端访问地址。后面的就走sso单点登录操作。
- 如果该用户已经在别的系统已经登录过。sso客户端拦截器拦截到该操作是单点授权验证后会重定向到sso服务端单点授权url。
4.sso服务端拦截器拦截到 单点授权url后 首先获取 cookie中 GrantingTicket的id 。然后根据GrantingTicket的id获取缓存中的GrantingTicket。 - sso服务端 生成ServiceTicket 并放入到GrantingTicket的缓存中。
- sso服务端 重定向到sso客户端并携带ServiceTicket 的id (ticket)。
- sso客户端拦截器拦截到重定向url并获取到ticket 。然后根据ticket从sso服务端获取用户的信息。
- sso服务端接受到sso客户端ticket 根据ticket 获取缓存中的 GrantingTicket的id,然后再根据GrantingTicket的id 获取GrantingTicket。然后再从GrantingTicket获取用户信息,返回给客户端。
- sso客户端接受到用户的信息后 将信息保存在session中
- 然后将 ticket 和sesion的关系对应保存在缓存中。
单点退出
- sso客户端点击退出按钮,会向sso服务端发起退出登录操作。
- sso服务端拦截器拦截到退出的操作后会将进入退出的操作逻辑
- sso服务端 第一步先获取sso服务端在存储的GrantingTicket的cookie信息
- 根据在cookie中存储的GrantingTicket的id 从缓存中获取到GrantingTicket。
- 然后将sso服务端在存储的GrantingTicket的cookie信息清除。
- 获取GrantingTicket 缓存的ServiceTicket缓存map信息。
- 遍历ServiceTicket的Map信息。
- 通过ServiceTicket的host信息依次调用客户端的退出url并携带ticket。
- sso客户端拦截器拦截到退出的操作并获取到ticket.根据之前在客户端缓存的map中根据ticket获取到sesion的信息并调用sess.invalidate();
- 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);
}
测试步骤:
- 点单登录Demo并没有对接mysql数据库 在进行部署测试的时候请使用 登录名:lijunkui 密码:123456 进行登录
- 访问 http://localhost:8090/ssoClient/index/index 或者
http://localhost:8090/ssoClient/login 进行登录 - 登录成功后会跳转到 http://localhost:8090/ssoClient/index/index。
- 然后再访问 http://localhost:8070/ssoClient2/index/index 进行测试是时候可以直接登录。