springboot session + redis整合 SSO初版

背景資料:

sso:

單點(diǎn)登錄原理與簡單實(shí)現(xiàn)
??推薦,要了解基礎(chǔ)原理的得看看。提供了思路,代碼細(xì)節(jié)待琢磨,使用的是springmvc

spring session:

請不要使用2.0.2.RELEASE版本,后面會有說明。
spring-boot+spring-session集成
通過Spring Session實(shí)現(xiàn)新一代的Session管理
Spring Session Strategy 詳解

redis:

springboot2.x redis緩存配置


補(bǔ)充說明:

  1. 對上面提供的sso博客原理補(bǔ)充(13/05/2018補(bǔ)充:cas單點(diǎn)登錄):
何為局部會話,全局會話,為什么訪問第二個系統(tǒng)時認(rèn)證中心能夠知道用戶已登錄?

局部會話:
??????瀏覽器訪問系統(tǒng)A或B時兩者間建立的一個會話,具體表現(xiàn)為系統(tǒng)A或B會為第一次訪問建立一個session對象,之后訪問不會再建立(session還存活時)
全局會話:
??????這個在博客中沒有說清楚,其實(shí)這個指的是瀏覽器與認(rèn)證中心之間建立的一個會話!

當(dāng)訪問第二個系統(tǒng)B時,系統(tǒng)B發(fā)現(xiàn)用戶沒登錄,重定向到認(rèn)證中心,注意此時會話切換到了全局會話!認(rèn)證中心通過該會話可以發(fā)現(xiàn)用戶已經(jīng)登陸(之前訪問系統(tǒng)A時到認(rèn)證中心登錄了)

  1. 對spring session補(bǔ)充:

如果用spring boot整合spring session來實(shí)現(xiàn)sso, 不要使用spring boot 2.0.1版本(允悲, 排查了好久)。
因?yàn)閟pring boot 2.0.1 順帶用了spring session 2.0.2.RELEASE,但是2.0.2版本的spring session文檔中說了下面一句英文:
貌似是說 該版本不支持 單個瀏覽器實(shí)例維持多個用戶會話(原理不太懂的話參照上面提供的鏈接)

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



===================華麗的分割線=======================


主要代碼(比較粗糙,后續(xù)可能改動):

sso server 認(rèn)證中心端:

提供一個Filter類,主要過濾處理各個系統(tǒng)的重定向。

@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;
        }

        //已經(jīng)登錄,然后根據(jù)其他參數(shù)進(jìn)行下一步判斷
        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;
        }

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

        //根據(jù)token, 緩存用戶
        redisTemplate.opsForValue().set("user:" + token, user);

        if (!StringUtils.isEmpty(clientUrl)) {

            // 緩存各個系統(tǒng)的地址
            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);

        // 當(dāng)request參數(shù)中token為空,可從session中獲取
        if (StringUtils.isEmpty(token)) {
            token = (String)session.getAttribute(AuthConst.TOKEN);
        }

        //銷毀session
        if (session != null) {
            session.invalidate();
        }

        //通知各個系統(tǒng)注銷
        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);
    }
}

關(guān)鍵配置:
配置實(shí)例化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,關(guān)鍵配置
    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 各個系統(tǒng)端:

提供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,說明已通過驗(yàn)證,將session標(biāo)記為已登錄
        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;
        }

        //沒有登錄將用戶請求重定向到認(rèn)證中心
        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();

        // 用戶發(fā)出logout請求,重定向到認(rèn)證中心處理
        if ("/logout".equals(request.getRequestURI())) {
            String token = (String)session.getAttribute(AuthConst.TOKEN);
            //附帶系統(tǒng)首頁
            response.sendRedirect("http://localhost:8080/logout" + "?" + AuthConst.TOKEN + "=" + token
                + "&" + AuthConst.CLIENT_URL + "=http://localhost:8081/index");
            return;
        }

        // 得到來自認(rèn)證中心發(fā)過來的注銷通知
        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。

待續(xù)....

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容