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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 一、前言   项目中用到的SSO,使用开源框架cas做的。简单的了解了一下cas,并学习了一下 单点登录的原理,有兴趣的同学也可以学习一下,写个demo玩一玩。 二、工程结构      我模拟了 sso的客户端和sso的服务端, sso-core中主要是一些sso需要的过滤器和工具类,缓存和session共享的一些XML配置文件,还有springmvc需要的一下jar包的管理。

一、前言

  项目中用到的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;
    
    @Override
    public 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.");
    }
    
    @Override
    public 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;
    }

    @Override
    public 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")
    @ResponseBody
    public 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.xsd
        http://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/

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
缓存 NoSQL Java
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(二)
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(二)
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(二)
|
前端开发 NoSQL 应用服务中间件
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(一)
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(一)
谷粒学院(十四)轮播图前后台 | 整合Redis | 单点登录(一)
|
存储 NoSQL Java
java基于Token+Redis的单点登录解决方案
本文介绍常用的三种中推荐使用的java基于Token+Redis的单点登录解决方案。
1699 0
java基于Token+Redis的单点登录解决方案
|
存储 负载均衡 NoSQL
(Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
(Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
849 0
(Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
|
NoSQL Redis
JavaWeb - 集群单点登录(Redis & Cookie & Filter)
JavaWeb - 集群单点登录(Redis & Cookie & Filter)
94 0
|
NoSQL Java Redis
基于redis+springboot从零开始设计一个类阿里系的单点登录(2)
基于redis+springboot从零开始设计一个类阿里系的单点登录(2)
基于redis+springboot从零开始设计一个类阿里系的单点登录(2)
|
存储 安全 NoSQL
基于redis+springboot从零开始设计一个类阿里系的单点登录(1)
基于redis+springboot从零开始设计一个类阿里系的单点登录(1)
基于redis+springboot从零开始设计一个类阿里系的单点登录(1)
|
存储 NoSQL 应用服务中间件
|
NoSQL Redis 应用服务中间件
基于SpringBoot+Redis的Session共享与单点登录
title: 基于SpringBoot+Redis的Session共享与单点登录date: 2019-07-23 02:55:52categories: 架构author: mrzhoutags:SpringBootredissession单点登录基于SpringBoot+Redis的Session共享与单点登录前言使用Redis来实现Session共享,其实网上已经有很多例子了,这是确保在集群部署中最典型的redis使用场景。
3175 0