• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

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

技术杂谈 勤劳的小蚂蚁 2个月前 (02-10) 122次浏览 已收录 0个评论 扫描二维码

一、前言
项目中用到的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