一、前言

  项目中用到的SSO,使用开源框架cas做的。简单的了解了一下cas,并学习了一下 单点登录的原理,有兴趣的同学也可以学习一下,写个demo玩一玩。

二、工程结构

  

  我模拟了 sso的客户端和sso的服务端, sso-core中主要是一些sso需要的过滤器和工具类,缓存和session共享的一些XML配置文件,还有springmvc需要的一下jar包的管理。sso-cache中配置了redis缓存策略。

三、单点登录原理图

  

  简单描述:

  用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  sso认证中心发现用户未登录,将用户引导至登录页面

  用户输入用户名密码提交登录申请

  sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌

  sso认证中心带着令牌跳转会最初的请求地址(系统1)

  系统1拿到令牌,去sso认证中心校验令牌是否有效

  sso认证中心校验令牌,返回有效,注册系统1

  系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源

  用户访问系统2的受保护资源

  系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌

  系统2拿到令牌,去sso认证中心校验令牌是否有效

  sso认证中心校验令牌,返回有效,注册系统2

  系统2使用该令牌创建与用户的局部会话,返回受保护资源

四、单点登录实现

  1.SSOFilter.java(sso client filter实现)

import java.io.IOException;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import com.alibaba.fastjson.JSONObject;
import com.hjz.sso.utils.RestTemplateUtil;public class SSOFilter implements Filter{public static Logger logger = LoggerFactory.getLogger(SSOFilter.class);private String SSO_SERVER_URL;private String SSO_SERVER_VERIFY_URL;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {SSO_SERVER_URL = filterConfig.getInitParameter("SSO_SERVER_URL");SSO_SERVER_VERIFY_URL = filterConfig.getInitParameter("SSO_SERVER_VERIFY_URL");if(SSO_SERVER_URL == null) logger.error("SSO_SERVER_URL is null.");if(SSO_SERVER_VERIFY_URL == null) logger.error("SSO_SERVER_VERIFY_URL is null.");}@Overridepublic void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;//请求中带有token,去sso-server验证token是否有效String authority = null;if(request.getParameter("token") != null) {boolean verifyResult = this.verify(request, SSO_SERVER_VERIFY_URL, request.getParameter("token"));if (verifyResult) {chain.doFilter(req, res);return;} else {authority = "token->" + request.getParameter("token") + " is invalidate.";}}HttpSession session = request.getSession();if (session.getAttribute("login") != null && (boolean)session.getAttribute("login") == true) {chain.doFilter(req, res);return;}//跳转至sso认证中心String callbackURL = request.getRequestURL().toString();StringBuilder url = new StringBuilder();url.append(SSO_SERVER_URL).append("?callbackURL=").append(callbackURL);if(authority != null) {url.append("&authority=").append(authority);}response.sendRedirect(url.toString());}private boolean verify(HttpServletRequest request, String verifyUrl, String token) {String result = RestTemplateUtil.get(request, verifyUrl + "?token=" + token, null);JSONObject ret = JSONObject.parseObject(result);if("success".equals(ret.getString("code"))) {return true;}logger.error(request.getRequestURL().toString() + " : " + ret.getString("msg"));return false;}@Overridepublic void destroy() {}}

View Code

  2.LoginController.java(sso server登录controller)

import java.util.UUID;import javax.servlet.http.HttpSession;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;@Controller
@RequestMapping("sso")
public class LoginController {private Logger logger = LoggerFactory.getLogger(LoginController.class);@RequestMapping(value="login", method={RequestMethod.GET, RequestMethod.POST})public String login(HttpSession session, Model model,@RequestParam(value="name", required=false) String name,@RequestParam(value="password", required=false) String password) {if(name == null && password == null) return "login";if("admin".equals(name) && "admin".equals(password)) {String token = UUID.randomUUID().toString();session.setAttribute("login", true);session.setAttribute("token", token);return "index";} else {model.addAttribute("error", true);model.addAttribute("message", "用户名或密码错误。");return "login";}}
}

View Code

  3.ValidateController.java(sso server验证token controller)

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;import com.alibaba.fastjson.JSONObject;@Controller
@RequestMapping("sso")
public class ValidateController {@RequestMapping("verify")@ResponseBodypublic JSONObject verify(HttpServletRequest request, @RequestParam String token) {HttpSession session = request.getSession();JSONObject result = new JSONObject();if(session.getAttribute("token") != null && token.equals(session.getAttribute("token"))) {result.put("code", "success");result.put("msg", "认证成功");} else {result.put("code", "failure");result.put("msg", "token已失效,请重新登录!");}return result;}}

View Code

  4.在sso client工程中加上SSOFilter(web.xml部分配置)

<filter><filter-name>ssoFilter</filter-name><filter-class>com.hjz.sso.filter.SSOFilter</filter-class><init-param><param-name>SSO_SERVER_URL</param-name><param-value>http://localhost:8088/sso-server/sso/login</param-value></init-param><init-param><param-name>SSO_SERVER_VERIFY_URL</param-name><param-value>http://localhost:8088/sso-server/sso/verify</param-value></init-param>
</filter><filter-mapping><filter-name>ssoFilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>

View Code

  基本模型已经出来了,启动sso-client 和 sso-server(本人都部署到了同一个tomcat下),试图去验证单点登录。测试的时候,从浏览器中的cookie中查看,可以看到 localhost域下有多个JSESSIONID。这也难怪, Tomcat中的每一个application都会创建自己的session会话。那接下来的事情就是解决 session 共享的问题,这样我们就可以完成我们的单点登陆了。

  为完成 session共享,这里推荐两种方案。一个是 tomcat+redis实现session共享,一个是 spring session+redis实现session共享。我这里采用了第二种方案,详情请接着看下面的步骤。

  5.为每个工程的web.xml中增加spring session代理filter的配置

<!-- session 代理 -->
<filter><filter-name>springSessionRepositoryFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter><filter-mapping><filter-name>springSessionRepositoryFilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>

  6.在sso-core中加入 缓存和spring session的xml配置(cache-config.xml)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"default-lazy-init="false"><description>Cache公共配置</description><bean id="cookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer"><property name="cookiePath" value="/"></property></bean><bean class="com.sso.cache.config.CacheConfig"/><bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"><property name="maxInactiveIntervalInSeconds" value="1800"></property></bean>
</beans>

  这里说一下为什么有定义一个 cookieSerializer 这个bean。参看RedisHttpSessionConfiguration的源码,发现它继承了SpringHttpSessionConfiguration,继续查看源码,发现SpringHttpSessionConfiguration中实现了我们配置的spring session代理filter,如下所示。

SpringHttpSessionConfiguration.java
@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);sessionRepositoryFilter.setServletContext(this.servletContext);if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy);} else {sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);}return sessionRepositoryFilter;
}

  查看源码,可以发现 SpringHttpSessionConfiguration使用的默认会话策略(httpSessionStrategy)是CookieHttpSessionStrategy。继续查看CookieHttpSessionStrategy的源码,如新建session写入cookie。

public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {Set sessionIdsWritten = getSessionIdsWritten(request);if (sessionIdsWritten.contains(session.getId())) {return;}sessionIdsWritten.add(session.getId());Map sessionIds = getSessionIds(request);String sessionAlias = getCurrentSessionAlias(request);sessionIds.put(sessionAlias, session.getId());String cookieValue = createSessionCookieValue(sessionIds);this.cookieSerializer.writeCookieValue(new CookieSerializer.CookieValue(request, response, cookieValue));
}

  cookieSerializer 默认是 DefaultCookieSerializer。查看DefaultCookieSerializer 的 writeCookieValue方法如下。

public void writeCookieValue(CookieSerializer.CookieValue cookieValue) {HttpServletRequest request = cookieValue.getRequest();HttpServletResponse response = cookieValue.getResponse();String requestedCookieValue = cookieValue.getCookieValue();String actualCookieValue = requestedCookieValue + this.jvmRoute;Cookie sessionCookie = new Cookie(this.cookieName, actualCookieValue);sessionCookie.setSecure(isSecureCookie(request)); sessionCookie.setPath(getCookiePath(request));String domainName = getDomainName(request);if (domainName != null) {sessionCookie.setDomain(domainName);}if (this.useHttpOnlyCookie) {sessionCookie.setHttpOnly(true);}if ("".equals(requestedCookieValue)) {sessionCookie.setMaxAge(0);} else {sessionCookie.setMaxAge(this.cookieMaxAge);}response.addCookie(sessionCookie);
}

  sessionCookie.setPath(getCookiePath(request));这块有一个问题,看一下getCookiePath方法的实现,如下。

private String getCookiePath(HttpServletRequest request) {if (this.cookiePath == null) {return request.getContextPath() + "/";}return this.cookiePath;
}

  如果要实现单点登录,就不要使用默认的 cookiePath 的值。所以,我定义了一个 cookieSerializer 的bean,并指定了 cookiePath 的值。 SpringHttpSessionConfiguration中如下方法可以自动装配 我们配置的cookieSerializer,而不是使用默认的。

@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);
}

  7.在每个工程中的spring公共配置文件中增加如下配置。

<import resource="classpath*:cache-config.xml"/>

  8.后端之间rest请求传递 session ID。

private static ResponseEntity<String> request(ServletRequest req, String url, HttpMethod method, Map<String, ?> params) {HttpServletRequest request = (HttpServletRequest) req;//获取header信息HttpHeaders requestHeaders = new HttpHeaders();Enumeration<String> headerNames = request.getHeaderNames();while (headerNames.hasMoreElements()) {String key = (String) headerNames.nextElement();String value = request.getHeader(key);requestHeaders.add(key, value);}HttpEntity<String> requestEntity = new HttpEntity<String>(params != null ? JSONObject.toJSONString(params) : null, requestHeaders);ResponseEntity<String> rss = restTemplate.exchange(url, method, requestEntity, String.class);return rss;
}

  使用RestTemplate发送rest请求,发送之前复制request中的header信息,保证session ID可以传递。

  9.最后,启动工程,测试结果如下。

   

  http://study.hujunzheng.cn:8000/sso-client-user/  和  http://study.hujunzheng.cn:8000/sso-client-org/ 切换访问工程。

五、完整项目地址

  https://github.com/hjzgg/SSO

六、项目演示地址

  http://study.hujunzheng.cn:8000/sso-client-user/

单点登录实现(spring session+redis完成session共享)相关推荐

  1. Spring Session Redis实现Session共享

    Spring Session & Redis实现Session共享 前后端分离Session.单点登录实现 Session 共享简单方案,充当笔记 一.Session和Cookie 1.Ses ...

  2. 单点登录与权限管理本质:session和cookie介绍

    本篇开始写「单点登录与权限管理」系列的第一部分:单点登录与权限管理本质,这部分主要介绍相关的知识概念.抽象的处理过程.常见的实现框架.通过这部分的介绍,能够对单点登录与权限管理有整体上的了解,对其相关 ...

  3. PHP多台服务实现单点登录,如何在多台服务器上共享Session(PHP/JSP/ASP.NET)以及单点登录(SSO)...

    如何在多台服务器上共享Session(PHP/JSP/ASP.NET)以及单点登录(SSO) Apache Session复制: 使用Memcached来共享PHP Session: 使用Cohere ...

  4. php java 单点登录_php实现多站点共用session实现单点登录的方法详解

    本文实例讲述了php实现多站点共用session实现单点登录的方法.分享给大家供大家参考,具体如下: 最近闲来无事,总结整理下单点登录的问题. 单点登录的基本原理为:客户端共享sesionid,服务器 ...

  5. redis使用及Spring Session+Redis实现Session共享

    1.redis的使用 首先引入依赖 IDEA可以直接创建项目前搜redis直接导入 <dependency><groupId>org.springframework.boot& ...

  6. 【分布式-session】spring boot + redis 实现session共享

    1 为何需要Session共享? 因为每个应用容器默认是将Session存储在本地内存,仅容器本身可以访问,但是在分布式集群中,包含多个应用容器,一个用户请求会随机发送到容器上,比如,用户在A容器登陆 ...

  7. okta-spring_通过Okta的单点登录保护Spring Boot Web App的安全

    okta-spring "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多 ...

  8. 通过Okta的单点登录保护Spring Boot Web App的安全

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 您可以使 ...

  9. 【SSO单点登录】ticket+token+redis 实现sso单点登录 防重放、防盗用、防篡改

最新文章

  1. 【camera-radar】自动驾驶相机-毫米波雷达融合方案综述
  2. Android三个Version的作用
  3. mysql主从备份失败--记录现象
  4. vue项目条形码和二维码生成工具试用
  5. unity 天空盒_Unity自定义可编程渲染管线(SRP)(二)——编写第一个自定义SRP
  6. 第10章 部署Exchange2010 部署边缘服务器
  7. S2-MLPV2:目前最强的视觉MLP架构,空降榜一,达到83.6% Top-1准确率
  8. PgSQL · 应用案例 · 经营、销售分析系统DB设计之共享充电宝
  9. sift计算描述子代码详解_浅谈SIFT特征描述子
  10. python包导入细节_python 如何上传包到pypi
  11. llustrator 教程,如何在 Illustrator 中对齐内容?
  12. jdbc封装工具类代码_【百战程序员从开始到植发】之JDBC
  13. 被欧美公司垄断的工业软件,中国还有机会吗?
  14. JuiceFS 如何帮助趣头条超大规模 HDFS 降负载
  15. Java项目:基于jsp+mysql+Spring+SpringMVC+mybatis的高速公路收费管理系统
  16. win10系统安装和优化
  17. hadoop namenode ha方案
  18. 京东商品关联版式,如何设置手机和电脑都显示?
  19. 高通新款高端芯片抢闸发布,小米将首发,趁机从华为手里抢夺市场
  20. PHOTOSHOP CS打造素描MM

热门文章

  1. centos根分区扩容方法linux公社,Centos5给/根分区扩容
  2. mysql性能测试工具msyqlslap_MySQL性能测试工具 mysqlslap
  3. 永磁同步电机三相等效电路图_基于MTPA的永磁同步电动机矢量控制系统分解
  4. 如何用数学和化学方法测量英国海岸线的长度
  5. Java编程之URI
  6. list agg cause ORA-06502 PL/SQL: numeric or value error
  7. springmvc 自定义view支持json和jsonp格式数据返回
  8. mysql六:索引原理与慢查询优化
  9. OpenCV中Mat的属性
  10. LeetCode--Factorial Trailing Zeroes(注意)