码迷,mamicode.com
首页 > 其他好文 > 详细

SSO服务源码分析

时间:2015-03-29 18:17:09      阅读:191      评论:0      收藏:0      [点我收藏+]

标签:

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




SSO服务源码分析

标签:

原文地址:http://my.oschina.net/kanlianhui/blog/393276

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!