美文网首页Java技术升华
springboot session + redis整合 SSO

springboot session + redis整合 SSO

作者: xiaoyiyiyo | 来源:发表于2018-05-08 21:17 被阅读262次

    背景资料:

    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

    相关文章

      网友评论

      本文标题:springboot session + redis整合 SSO

      本文链接:https://www.haomeiwen.com/subject/edgwrftx.html