基于CAS协议的单点登录(SSO)
程序员文章站
2022-06-11 11:14:09
...
基于CAS协议的单点登录(SSO)
1.单点登录
单点登陆(SSO,Single Sign On)简单地说,单点登陆允许多个应用使用同一个登陆服务。一旦一个用户登陆了一个支持单点登陆的应用,那么在进入其它使用同一单点登陆服务的应用时就不再需要重新登陆了。
例如:当用户登录京东商城以后在进入京东秒杀系统,京东金融系统就不需要在重新登录了。这就是单点登录。
单点登录的实现方式有基于cookie实现和基于CAS协议实现。
基于cookie实现的必要条件是 要是先单点登录的服务必须在同一域名下才能实现cookie共享。
如果域名不同的情况下是不能基于cookie实现单点登录的。因为不同域名的cookie不能共享。
2.CAS协议
它是一个开源的单点登录系统(SSO)。实现的机制不算复杂但是思想十分灵巧。用CAS也可以快速实现单点登录。
CAS主要分为CAS Client 和CAS Server ,其中Client主要是内嵌在需要SSO登录站点的拦截器或过滤器上。
- 首先浏览器向站点1发起请求。
- 站点1发现当前请求没有合法的Cookie,那么重定向到CAS Server上,也就是SSO Server。
- CAS Server展示登录界面,要求用户登录。
用户登录后,会写CAS Server的Cookie到浏览器,同时生产ticket,利用一个302跳转到CASClient。这样能保证用户无感知。 - CAS Client利用生成的ticket发送到CAS Server进行验证,验证通过后,站点1生成自己的Cookie并回写到用户浏览器,然后进行登录成功的跳转。
- 这样就能保证当前浏览器在站点1的域名下,有站点1的Cookie,同时当前浏览器也有CAS Server的Cookie。
- 站点2,在进行登录时和站点1初次登录流程一致,但是在访问CAS Server的时候,由于当前浏览器已经有了CAS Server的Cookie,那么直接校验通过返回ticket。
ticket通过302跳转跳转到CAS Client上,之后的流程就和站点1是一样的了。如果此时认证失败,那么需要重新走一次登录的过程。
其实感觉很麻烦,但是流程却十分的简单,主要是使用CAS Server的Cookie做校验,同时各自系统维护自己的Cookie。
3.注意的问题
- CAS Server的Cookie劫持问题,如果CAS Server的Cookie被劫持掉,那么就相当于拿到了一切,所以必须要用HTTPS实现这个过程。
- ticket的使用,ticket只能被使用一次,一次校验后立即失效。同时需要有时效性,一般5分钟。最后ticket生成规则要随机,不能被碰撞出来。
-
对于各自系统自己的Session,也可以依赖于SSO,这样就能保证所有的Session规则一致,便于集中控制。
4.手写代码实现
SSOServer端关键代码
package com.lyyz.controller;
import com.lyyz.entity.UserEntity;
import com.lyyz.service.LoginService;
import com.lyyz.util.MD5Generator;
import net.sf.json.JSON;
import net.sf.json.JSONObject;
import net.sf.json.JSONString;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
/**
* Created by lyyz on 2017/8/21.
*/
public class LoginController extends HttpServlet {
private LoginService loginService = new LoginService();
public static Map<String, String> tickets = new HashMap<String, String>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String uri = req.getRequestURI();
//检查client端通过httpClient 发送get请求检查 ticket是否有效
//注意 通过Http请求过来的 不可以用登录步骤的Session信息
// 可以将ticket信息存在内存里 最好放入redis里或者memcached里。
if(uri.indexOf("checkticket")>-1){
PrintWriter print = resp.getWriter();
String clientServer = req.getParameter("clientServer");
String key = req.getParameter("key");
//从内存中的Map中取ticket数据进行校验
String ticket = tickets.get(clientServer);
HashMap<String,String> result = new HashMap<String, String>();
//返回信息
if(ticket.equals(key)){
result.put("rc","0");
//用户的授权信息可以 从redis中取出来 这里登录时候存的Session信息是不能用的
result.put("username","admin");
print.print(JSONObject.fromObject(result));
tickets.remove(clientServer);
}else{
result.put("rc","-1");
print.print(JSONObject.fromObject(result));
}
print.flush();
print.close();
}else{
//登录的Get请求处理
String returnUrl = req.getParameter("returnUrl");
String clientServer = req.getParameter("clientServer");
req.getSession().setAttribute("returnUrl", URLDecoder.decode(returnUrl));
req.getSession().setAttribute("clientServer", clientServer);
UserEntity userEntity = (UserEntity) req.getSession().getAttribute("user");
//session 登录信息处理 没有登录跳转登录页面
if(userEntity==null ||userEntity.getId()==null || "".equals(userEntity.getId())) {
req.getRequestDispatcher("login.jsp").forward(req, resp);
}else{
//如果有登录信息 直接带ticket值 回跳到client端
String key = MD5Generator.generate( clientServer+UUID.randomUUID().toString());
resp.sendRedirect(returnUrl+"?key="+key);
tickets.put(clientServer,key);
}
}
}
//登录的Post请求
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
String kaptchaExpected = (String)session.getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
String code = req.getParameter("code");
String loginName = req.getParameter("loginName");
String password = req.getParameter("password");
String rememberMe = req.getParameter("rememberMe");
PrintWriter pw = resp.getWriter();
//验证码校验
if(!code.toLowerCase().equals(kaptchaExpected.toLowerCase())){
pw.write("code error");
}
//登录成功 存储用户信息
UserEntity result = loginService.login(loginName,password);
session.setAttribute("user",result);
String returnUrl = (String)session.getAttribute("returnUrl");
String clientServer = (String)session.getAttribute("clientServer");
String key = MD5Generator.generate(clientServer+UUID.randomUUID());
// 生成ticket 存储到内存中, 千万不要存入session中,错误的。
tickets.put(clientServer,key);
//带ticket值回跳到 client
resp.sendRedirect(returnUrl+"?key="+key);
}
}
SSOClient 代码
package com.lyyz.filter;
import com.lyyz.entity.UserEntity;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URLEncoder;
/**
* Created by lyyz on 2017/8/24.
*/
public class SSOClientFilter implements Filter {
private static final String SSOSERVERURL = "http://localhost:8081/ssoserver/login";
private static final String RETURNURL = "http://localhost:8082/ssojava/ticket";
private static final String CLIENTSERVER = "ssojava";
/**
* 登录拦截实现
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String uri = httpServletRequest.getRequestURI();
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
HttpSession httpSession = httpServletRequest.getSession();
//访问登录方法,登录jsp放行
if(uri.indexOf("static")>-1 || uri.indexOf("Kaptcha")>-1 || uri.indexOf("ticket")>-1){
chain.doFilter(request, response); //让目标资源执行,放行
return;
}
String ticket = (String) httpSession.getAttribute("ticket");
UserEntity userEntity = (UserEntity) httpSession.getAttribute("user");
//session 登录信息处理
if(userEntity==null ||userEntity.getId()==null || "".equals(userEntity.getId())){
httpServletResponse.sendRedirect(SSOSERVERURL+"?returnUrl="+URLEncoder.encode(RETURNURL)+"&clientServer="+CLIENTSERVER);
}else{
chain.doFilter(request, response); //让目标资源执行,放行
}
}
}
package com.lyyz.controller;
import net.sf.json.JSONObject;
import net.sf.json.JSONString;
import net.sf.json.util.JSONBuilder;
import net.sf.json.util.JSONStringer;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
//client 接收到server的key值后 通过HttpClient向server端发送http请求 校验ticket的有效性 并且返回用户的授权信息 将信息存入cookie中
public class TicketController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
System.out.println("ticket==============="+key);
HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost:8081/ssoserver/login/checkticket?key="+key+"&clientServer=ssojava");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int l;
byte[] tmp = new byte[2048];
while ((l = instream.read(tmp)) != -1) {
}
System.out.println("验证ticket==========="+new String(tmp));
String result = new String(tmp);
JSONObject jsonObject = JSONObject.fromObject(result);
System.out.println(jsonObject.get("rc"));
//
}
}
}
上述代码是自己通过CAS协议理解写的,不足之处请指教。谢谢
上一篇: 万能的CURD