• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

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

一、前言
项目中用到的SSO,使用开源框架 cas 做的。简单的了解了一下 cas,并学习了一下 单点登录的原理,有兴趣的同学也可以学习一下,写个 demo 玩一玩。
二、工程结构
我模拟了 sso 的客户端和 sso 的服务端, sso-core 中主要是一些 sso 需要的过滤器和工具类,缓存和 session 共享的一些 XML 配置文件,还有 springmvc 需要的一下 jar 包的管理。sso-cache 中配置了 redis 缓存策略。
三、单点登录原理图
简单描述:
  1. 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  2. sso 认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso 认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso 认证中心带着令牌跳转会最初的请求地址(系统 1)
  6. 系统 1 拿到令牌,去 sso 认证中心校验令牌是否有效
  7. sso 认证中心校验令牌,返回有效,注册系统 1
  8. 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统 2 的受保护资源
  10. 系统 2 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  11. sso 认证中心发现用户已登录,跳转回系统 2 的地址,并附上令牌
  12. 系统 2 拿到令牌,去 sso 认证中心校验令牌是否有效
  13. sso 认证中心校验令牌,返回有效,注册系统 2
  14. 系统 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;
publicclassSSOFilterimplementsFilter{
  publicstatic Logger logger = LoggerFactory.getLogger(SSOFilter.class);
 
  private String SSO_SERVER_URL;
  private String SSO_SERVER_VERIFY_URL;
 
  @Override
  publicvoidinit(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.”);
  }
 
  @Override
  publicvoiddoFilter(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());
  }
 
  privatebooleanverify(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”))) {
          returntrue;
      }
      logger.error(request.getRequestURL().toString() + ” : “ + ret.getString(“msg”));
      returnfalse;
  }
  @Override
  publicvoiddestroy(){
  }
}
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”)
publicclass LoginController {
   private Logger logger = LoggerFactory.getLogger(LoginController.class);
   
   @RequestMapping(value=“login”, method={RequestMethod.GET, RequestMethod.POST})
   publicString 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”;
       }
   }

}

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”)
publicclass ValidateController {
   
   @RequestMapping(“verify”)
   @ResponseBody
   public JSONObject verify(HttpServletRequest request, @RequestParamString 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;
   }
   
}
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>
基本模型已经出来了,启动 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”?>
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd&#8221;
      default-lazy-init=“false”>
   <description>Cache 公共配置</description>
   <beanid=“cookieSerializer”class=“org.springframework.session.web.http.DefaultCookieSerializer”>
       <propertyname=“cookiePath”value=“/”></property>
   </bean>
   
   <beanclass=“com.sso.cache.config.CacheConfig”/>
   
   <beanclass=“org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration”>
       <propertyname=“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 方法如下。
publicvoid 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() + “/”;
   }
   returnthis.cookiePath;
}
如果要实现单点登录,就不要使用默认的 cookiePath 的值。所以,我定义了一个 cookieSerializer 的 bean,并指定了 cookiePath 的值。 SpringHttpSessionConfiguration 中如下方法可以自动装配 我们配置的 cookieSerializer,而不是使用默认的。
@Autowired(required = false)
publicvoidsetCookieSerializer(CookieSerializer cookieSerializer){
   this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);

}

7.在每个工程中的 spring 公共配置文件中增加如下配置。
<import resource=“classpath*:cache-config.xml”/>
8.后端之间 rest 请求传递 session ID。
privatestatic 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.最后,启动工程,测试结果如下。
两个链接来切换访问工程。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:单点登录实现(spring session+redis 完成 session 共享)
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00