博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SSO服务源码分析
阅读量:6082 次
发布时间:2019-06-20

本文共 6469 字,大约阅读时间需要 21 分钟。

hot3.png

        SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

        实现单点登录的实质就是要解决如何产生和存储信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下几个:

  • 存储信任
  • 验证信任

只要解决了以上的问题,达到了开头讲得效果就可以说是SSO。最简单实现SSO的方法就是用Cookie,实现流程如下所示:

    但是目前Cookie的实现存在两个问题:

  • Cookie不安全
  • 不能跨域免登

第一个问题可以通过对Cookie来处理,第二个问题却是硬伤了。

SSO的实现除了Cookie之外还有许多实现方式,这里暂且分析一下一个基于Cookie的实现源码。

首先给出本次分析的结论,具体源码贴在结论之后。

具体实现逻辑总结

  • 设置web应用的filter,用于初始化每次请求线程的SSOInfo(其中包含 登陆有效性的ticket用户的业务数据(比如userId)本次请求的HttpServletRequest和HttpServletResponse(可选)。
  • 读取Cookies获取ticket放入SSOInfo,无则ticket为null
  • 把SSOInfo存储进一个线程隔离级别的容器中(这里使用ThreadLocal实现)
  • 在需要拦截的Controller前设置拦截器,对SSOInfo中包含的ticket进行有效性校验。(这里的有效性校验实现方式很多,本次系统中是用redis来存储、管理ticket有效性的)
  • 无效ticket的情况下引导登陆,创建ticket(由特定字段与UUID.randomUUID(); 来实现),保存(redis作为key存储value为用户id),管理(redis设置超时规则)ticket。

以上就是这个SSO系统的具体实现逻辑。分析出来实现逻辑比较简单。可适用于一般的小型单域名的网站。

以下为具体实现代码:

web项目设置初始化filter

web.xml

AuthFilter
com.jc.sso.client.AuthFilter
AuthFilter
/*

AuthFilter:读取cookies初始化SSOInfo

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {    SSOInfo info = SSOCookieUtil.vistSsoCookie((HttpServletRequest)request);  SsoManager.setSSOInfo(info);  info.setRequestObj((HttpServletRequest)request);  info.setResponseObj((HttpServletResponse)response);  try {  // pass the request along the filter chain  chain.doFilter(request, response);  } finally {  SsoManager.clearSSOInfo();  }  }

SSOInfo

记录SSO的ticket的bean,为了额外信息的获取,同时记录了HttpServletRequest和HttpServletResponse(这里只是为了额外信息的记录,比如访问Ip地址等等)。

private User user;  private String ticket;  private String uid;//标识pc主机的id    private String app;  /**   * 是否已经进行过ticket的校验   */  private boolean isValidated = false;    private HttpServletRequest requestObj;  private HttpServletResponse responseObj;    public boolean isLogin() {  if (ticket == null) {  return false;  }  if (!isValidated) {  throw new NotValidateException();  }    return user != null;  }

SSOCookieUtil

一个Cookie的操作类

public static SSOInfo vistSsoCookie(HttpServletRequest request) {  Cookie[] cookies = getAllCookies(request);  if(cookies == null || cookies.length == 0){  return new SSOInfo(null);  }  String ticket = null;  String uid = "none";  for(Cookie cookie : cookies){  if(TICKET_GRANT_TICKET_COOKIE.equals(cookie.getName())){  ticket = cookie.getValue();  } else if(UID_COOKIE.equals(cookie.getName())){  ticket = cookie.getValue();  }  }    SSOInfo si = new SSOInfo(ticket);  si.setUid(uid);  String app = SsoManager.config.getValue(SsoManager.CONFIG_APP_ID);  si.setApp(app);    return si;  }

初次访问会返回一个ticket为null的SSOInfo。

存储线程级别的SSOInfo。(注意,上面是在filter中进行的初始化,此时请求继续分发)

SsoManager.setSSOInfo(info);

SsoManager

private static ThreadLocal
tempStore = new ThreadLocal
(); public static SSOInfo getSSOInfo() { return tempStore.get(); } public static void setSSOInfo(SSOInfo info) { tempStore.set(info); }

在请求中设置拦截器

UserAccountInterceptor.preHandle

if (SsoManager.validateWebTicket() ) {//登陆状态     String userId = SsoManager.getSSOInfo().getUser().getUserId();

验证登录状态

public static boolean validateWebTicket() {  SSOInfo si = tempStore.get();  if (si == null) {  logger.warn("The ssoinfo object is missed, check whether some unexpected operation on ThreadLocal is executed!");  return false;  }  validate2Server(si);  return si.isLogin();  }

validate2Server

private static void validate2Server(SSOInfo si) {  if (si == null) {  return;  }    if (si.isValidated()) {  return ;  }    if (si.getTicket() == null || si.getTicket().length() == 0) {  si.setValidated(true);  return;  }  …………(后台是远程调用验证系统传入ticket)    }

验证系统

@RequestMapping(value = "validate.html")  @ResponseBody  public String validateLogin(@RequestParam(value="t", required=false)String ticket, String app,  @RequestParam(value="did", required=false)String deviceId) {  if (StringUtils.isEmpty(ticket)) {  return setErrorView("ticket值为空");  }else if(StringUtils.isEmpty(app)) {  return setErrorView("app类型不能为空");  } else if(StringUtils.isEmpty(deviceId)) {  return setErrorView("设备ID为空");  }  try {  String user = authManager.checkTGT(ticket, app);  if (user != null) {  return buildSuccessResponse(user);  } else {  return buildErrorResponse(user);  }    } catch (Exception e) {  logger.error("登录异常(Unexpected)", e);  return setErrorView("服务异常,请稍后再试");  }  }

这里以ticket作为key来从redis中获取userId的信息

public String checkTGT(String tgt, String app) {  String user = null;  try{  user = redisTemplate.get(tgt);  int tmpApp = StringUtil.getIntValue(app, SSOConstant.APP_SITE);  if(!StringUtils.isEmpty(user)){  prolongTicket(tgt,app, user, getValiateTime(tmpApp));  }    }catch(Exception e){  logger.error("检查TGT异常:",e);  }  return user;  }

以上就是认证的整个流程,下面是登陆流程

try {  userInDb = loginServiceImpl.login(user);  } catch (PasswordNotMatchException e) {  if (logger.isInfoEnabled()) {  logger.info("登录失败,密码错误");  }  }     //4. 登陆成功情况下,生成ticket  user.setType(SSOConstant.APP_SITE);  String ticket = authManager.generateSiteTGT(request, response, "" + user.getType(), userInDb);

public String generateSiteTGT(HttpServletRequest request,HttpServletResponse response, String app, User user) {  String tgt = null;  try{  UUID uuid = UUID.randomUUID();  tgt = app + "-" + SSOConstant.TICKET_GRANT_TICKET + "-" + uuid.toString().replaceAll("-", "");  int tmpApp = StringUtil.getIntValue(app, SSOConstant.APP_SITE);  int longLogin = getValiateTime(tmpApp);  setupTicket(tgt, app, "", user, longLogin);    Cookie ticket = new Cookie(SSOConstant.TICKET_GRANT_TICKET_COOKIE, tgt);  String domain = PropertiesUtil.getString(SSOConstant.PROPERY_DOMAIN);  ticket.setDomain(domain);  ticket.setPath("/");  ticket.setMaxAge(longLogin);    response.addCookie(ticket);  }catch(Exception e){  logger.error("生成TGT异常:",e);  }  return tgt;  }

private void setupTicket(String ticket, String app, String deviceId, User user, int longLogin) {  if (ticket == null) {  return ;  }    if(longLogin < 1){  //保存30分钟  longLogin = SSOConstant.TICKET_GRANT_TICKET_TIME_OUT_DEFAULT;  }  String oldTicket = redisTemplate.get(app + "_" + user.getUserId());  if (oldTicket != null) {  redisTemplate.delKey(oldTicket);  }  redisTemplate.setex(ticket, longLogin, user.getId() + ":" + user.getUserId());  redisTemplate.setex(app + "_" + user.getUserId(), longLogin, ticket);  }

转载于:https://my.oschina.net/kanlianhui/blog/393276

你可能感兴趣的文章
Mysql利用binlog恢复数据
查看>>
我的友情链接
查看>>
用yum安装mariadb
查看>>
一点IT"边缘化"的人的思考
查看>>
WPF 降低.net framework到4.0
查看>>
搭建一个通用的脚手架
查看>>
开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
查看>>
开源磁盘加密软件VeraCrypt教程
查看>>
本地vs云:大数据厮杀的最终幸存者会是谁?
查看>>
阿里云公共镜像、自定义镜像、共享镜像和镜像市场的区别 ...
查看>>
shadowtunnel v1.7 发布:新增上级负载均衡支持独立密码
查看>>
Java线程:什么是线程
查看>>
mysql5.7 创建一个超级管理员
查看>>
【框架整合】Maven-SpringMVC3.X+Spring3.X+MyBatis3-日志、JSON解析、表关联查询等均已配置好...
查看>>
要想成为高级Java程序员需要具备哪些知识呢?
查看>>
带着问题去学习--Nginx配置解析(一)
查看>>
onix-文件系统
查看>>
java.io.Serializable浅析
查看>>
我的友情链接
查看>>
多线程之线程池任务管理通用模板
查看>>