SSO服务源码分析

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

<filter>
    <filter-name>AuthFilter</filter-name>
    <filter-class>com.jc.sso.client.AuthFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AuthFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping> 

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<SSOInfo> tempStore = new ThreadLocal<SSOInfo>(); 

    public static SSOInfo getSSOInfo() {
 return tempStore.get();
 } 

 public static void setSSOInfo(SSOInfo info) {
 tempStore.set(info);
 } 

在请求中设置拦截器


<mvc:interceptors>
     <mvc:interceptor>
        <mvc:mapping path="/user/*.html" />
        <bean class="com.jc.site.common.interceptor.UserAccountInterceptor"></bean>
     </mvc:interceptor>
</mvc:interceptors>    

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);
 } 

时间: 2024-10-12 17:35:42

SSO服务源码分析的相关文章

activitymanagerservice服务源码分析

activitymanagerservice服务源码分析 1.ActivityManagerService概述 ActivityManagerService(以下简称AMS)作为android中最核心的服务,主要负责系统的四大组件的启动.切换.调度以及应用进程的管理和调度等工作.它类似于操作系统中的进程管理和调度模块类似,所以要想掌握android,AMS至关重要.AMS属于service的一种,所以它也是由system_server进行启动以及管理.本文将以两条不同的主线来分析AMS:第一条与

http服务源码分析

多读go的源码,可以加深对go语言的理解和认知,今天分享一下http相关的源码部分 在不使用第三方库的情况下,我们可以很容易的的用go实现一个http服务, package main import ( "fmt" "net/http" ) func IndexHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello world ! ") } func main

Android的软件包管理服务PackageManagerService源码分析

Android系统下的apk程序都是通过名为PackageManagerService的包管理服务来管理的.PacketManagerService是安卓系统的一个重要服务,由SystemServer启动,主要实现apk程序包的解析,安装,更新,移动,卸载等服务.不管是系统apk(/system/app),还是我们手工安装上去的,系统所有的apk都是由其管理的. 以android 4.0.4的源码为例,android4.0.4/frameworks/base/services/java/com/

Spring Cloud Eureka服务注册源码分析

Eureka是怎么work的 那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上.通过下面的源码分析,看出Eureka Client的定时任务调用Eureka Server的Reset接口,而Eureka接收到调用请求后会处理服务的注册以及Eureka Server中的数据同步的问题. 服务注册 源码分析,看出服务注册可以认为是Eureka client自己完成,不需要服务本身来关心. Eureka Client的定时任务调用Eureka Se

dubbox源码分析(一)-服务的启动与初始化

程序猿成长之路少不了要学习和分析源码的.最近难得能静得下心来,就针对dubbox为目标开始进行源码分析. [服务提供方] 步骤 调用顺序 备注 容器启动 com.alibaba.dubbo.container.Main.main(args);dubbo.properties -> dubbo.container -> container.start()container -> spring, log4j, jetty... [dubbo-container-spring] SpringC

netty 5 alph1源码分析(服务端创建过程)

参照<Netty系列之Netty 服务端创建>,研究了netty的服务端创建过程.至于netty的优势,可以参照网络其他文章.<Netty系列之Netty 服务端创建>是 李林锋撰写的netty源码分析的一篇好文,绝对是技术干货.但抛开技术来说,也存在一些瑕疵. 缺点如下 代码衔接不连贯,上下不连贯. 代码片段是截图,对阅读代理不便(可能和阅读习惯有关) 本篇主要内容,参照<Netty系列之Netty 服务端创建>,梳理出自己喜欢的阅读风格. 1.整体逻辑图 整体将服务

Tomcat7.0源码分析——启动与停止服务

前言 熟悉Tomcat的工程师们,肯定都知道Tomcat是如何启动与停止的.对于startup.sh.startup.bat.shutdown.sh.shutdown.bat等脚本或者批处理命令,大家一定知道改如何使用它,但是它们究竟是如何实现的,尤其是shutdown.sh脚本(或者shutdown.bat)究竟是如何和Tomcat进程通信的呢?本文将通过对Tomcat7.0的源码阅读,深入剖析这一过程. 由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以startup.s

zookeeper源码分析之五服务端(集群leader)处理请求流程

leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcessor -> ProposalRequestProcessor ->CommitProcessor -> Leader.ToBeAppliedRequestProcessor ->FinalRequestProcessor 具体情况可以参看代码: @Override protected v

zookeeper源码分析之一服务端处理请求流程

上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析各自一下消息处理过程: 前文可以看到在 1.在单机情况下NettyServerCnxnFactory中启动ZookeeperServer来处理消息: public synchronized void startup() { if (sessionTracker == null) { createSe