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