springboot session + redis整合 SSO初版

背景资料:

sso:

单点登录原理与简单实现
  推荐,要了解基础原理的得看看。提供了思路,代码细节待琢磨,使用的是springmvc

spring session:

请不要使用2.0.2.RELEASE版本,后面会有说明。
spring-boot+spring-session集成
通过Spring Session实现新一代的Session管理
Spring Session Strategy 详解

redis:

springboot2.x redis缓存配置


补充说明:

  1. 对上面提供的sso博客原理补充(13/05/2018补充:cas单点登录):
何为局部会话,全局会话,为什么访问第二个系统时认证中心能够知道用户已登录?

局部会话:
      浏览器访问系统A或B时两者间建立的一个会话,具体表现为系统A或B会为第一次访问建立一个session对象,之后访问不会再建立(session还存活时)
全局会话:
      这个在博客中没有说清楚,其实这个指的是浏览器与认证中心之间建立的一个会话!

当访问第二个系统B时,系统B发现用户没登录,重定向到认证中心,注意此时会话切换到了全局会话!认证中心通过该会话可以发现用户已经登陆(之前访问系统A时到认证中心登录了)

  1. 对spring session补充:

如果用spring boot整合spring session来实现sso, 不要使用spring boot 2.0.1版本(允悲, 排查了好久)。
因为spring boot 2.0.1 顺带用了spring session 2.0.2.RELEASE,但是2.0.2版本的spring session文档中说了下面一句英文:
貌似是说 该版本不支持 单个浏览器实例维持多个用户会话(原理不太懂的话参照上面提供的链接)

11.4. Dropped Support
As a part of the changes to HttpSessionStrategy and it’s alignment to the counterpart from the reactive world, the support for managing multiple users' sessions in a single browser instance has been removed. This introduction of new API to replace this functionality in consideration for future releases.

故使用1.5.12版本的spring boot。

代码 github地址: https://github.com/xiaoyiyiyo/springboot_sso



===================华丽的分割线=======================


主要代码(比较粗糙,后续可能改动):

sso server 认证中心端:

提供一个Filter类,主要过滤处理各个系统的重定向。

@WebFilter(filterName = "sessionFilter", urlPatterns = {"/login", "/logout"})
public class SessionFilter implements Filter{

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session  = request.getSession();

        String uri = request.getRequestURI();

        //注销请求,放行
        if ("/logout".equals(uri)) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //已经登录,然后根据其他参数进行下一步判断
        if (session.getAttribute(AuthConst.IS_LOGIN) != null) {
            String clientUrl = request.getParameter(AuthConst.CLIENT_URL);
            String token = (String) session.getAttribute(AuthConst.TOKEN);
            if (clientUrl != null && !"".equals(clientUrl)) {
                redisTemplate.opsForSet().add(token, clientUrl);
                if (clientUrl.contains("?")) {
                    clientUrl = clientUrl + "&";
                } else {
                    clientUrl = clientUrl + "?";
                }
                response.sendRedirect(clientUrl + AuthConst.TOKEN + "=" + token);
                return;
            }
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //登录请求,放行
        if ("/".equals(uri) || "/login".equals(uri)) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //其他请求
        response.sendRedirect("/");
    }

    @Override
    public void destroy() {

    }
}

提供controller类,处理登录/注销请求:

@Controller
public class UserController {

    @Autowired
    private IUserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/login")
    public void login(HttpServletRequest request, HttpServletResponse response,
                        @RequestParam Map<String, String> map) throws IOException {

        String userName = map.get("username");
        String password = map.get("password");
        String clientUrl = map.get("clientUrl");

        UserDo user = userService.getUser(userName, password);

        if (user == null) {
            response.sendRedirect("/");
            return;
        }

        //设置全局session,并缓存
        HttpSession session = request.getSession();
        String token = UUID.randomUUID().toString();
        session.setAttribute(AuthConst.IS_LOGIN, true);
        session.setAttribute(AuthConst.TOKEN, token);

        //根据token, 缓存用户
        redisTemplate.opsForValue().set("user:" + token, user);

        if (!StringUtils.isEmpty(clientUrl)) {

            // 缓存各个系统的地址
            redisTemplate.opsForSet().add(AuthConst.CLIENT_URL + ":"+ token, clientUrl);

            if (clientUrl.contains("?")) {
                clientUrl = clientUrl + "&";
            } else {
                clientUrl = clientUrl + "?";
            }
            response.sendRedirect(clientUrl + AuthConst.TOKEN + "=" + token);
            return;
        }
        response.sendRedirect("/");
        return;
    }

    @GetMapping("/logout")
    public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession();
        String token = request.getParameter(AuthConst.TOKEN);
        String clientUrl = request.getParameter(AuthConst.CLIENT_URL);

        // 当request参数中token为空,可从session中获取
        if (StringUtils.isEmpty(token)) {
            token = (String)session.getAttribute(AuthConst.TOKEN);
        }

        //销毁session
        if (session != null) {
            session.invalidate();
        }

        //通知各个系统注销
        Set<String> set = redisTemplate.opsForSet().members(AuthConst.CLIENT_URL + ":" + token);
        if (null != set && set.size() > 0) {
            Map<String, String> paramMap = new HashMap<String, String>();
            paramMap.put(AuthConst.LOGOUT_REUQEST, token);
            for (String url: set) {
                HttpUtils.doPost(url, paramMap);
            }
        }

        if (StringUtils.isEmpty(clientUrl)) {
            response.sendRedirect("/");
        }
        response.sendRedirect(clientUrl);
    }
}

关键配置:
配置实例化CookieHttpSessionStrategy

@Configuration
public class RedisSessionConfig {

    @Bean
    public CookieHttpSessionStrategy cookieHttpSessionStrategy() {
        CookieHttpSessionStrategy strategy=new CookieHttpSessionStrategy();
        DefaultCookieSerializer cookieSerializer=new DefaultCookieSerializer();
        cookieSerializer.setCookieName("SSO_SESSION");//cookies名称
        cookieSerializer.setCookieMaxAge(1800);//过期时间(秒)
        strategy.setCookieSerializer(cookieSerializer);
        return strategy;
    }
}

配置application.yml

spring:
  session:
    store-type: redis #表示启用redis管理session,关键配置
    redis:
      namespace: sso_server
  cache:
    type: redis
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 0
    database: 1
    pool:
      max-active: 8
      max-wait: -1
      max-idle: 8
      min-idle: 0
  #jpa 配置
  jpa:
    hibernate:
      #命名策略(遇到大写字母,加"_"命名)
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
      ddl-auto: update
    show-sql: true
    database: mysql
  datasource:
    url: jdbc:mysql://localhost:3306/sso?useUnicode=true&characterEncoding=UTF8
    username: root
    password: xxxx
    driver-class-name: com.mysql.jdbc.Driver
sso client 各个系统端:

提供LoginFilter,过滤处理浏览器的访问之前是否已登录

@WebFilter(filterName = "loginFilter", urlPatterns = "/*")
public class LoginFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session = request.getSession();

        if ("/index".equals(request.getRequestURI())) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //第一次访问,用户没登录
        //用户拿到token,再次访问,此时is_login依旧为false
        Object is_login = session.getAttribute(AuthConst.IS_LOGIN);
        if (is_login != null && (Boolean) session.getAttribute(AuthConst.IS_LOGIN)) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //第一次访问,token为null
        //用户拿到token,说明已通过验证,将session标记为已登录
        String token = request.getParameter(AuthConst.TOKEN);
        if (token != null) {
            session.setAttribute(AuthConst.TOKEN, token);
            session.setAttribute(AuthConst.IS_LOGIN, true);
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //没有登录将用户请求重定向到认证中心
        response.sendRedirect("http://localhost:8080/login" + "?" + AuthConst.CLIENT_URL + "=" + request.getRequestURL());
    }

    @Override
    public void destroy() {

    }
}

提供LogoutFilter,过滤处理注销请求

/**
 * Created by xiaoyiyiyo on 2018/5/6.
 */
@WebFilter(filterName = "logoutFilter", urlPatterns = "/*")
public class LogoutFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session = request.getSession();

        // 用户发出logout请求,重定向到认证中心处理
        if ("/logout".equals(request.getRequestURI())) {
            String token = (String)session.getAttribute(AuthConst.TOKEN);
            //附带系统首页
            response.sendRedirect("http://localhost:8080/logout" + "?" + AuthConst.TOKEN + "=" + token
                + "&" + AuthConst.CLIENT_URL + "=http://localhost:8081/index");
            return;
        }

        // 得到来自认证中心发过来的注销通知
        String token = request.getParameter(AuthConst.LOGOUT_REUQEST);
        if (!StringUtils.isEmpty(token) && session != null) {
            session.invalidate();
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

主要配置参看sso server。

待续....

github地址: https://github.com/xiaoyiyiyo/springboot_sso

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容