分布式Session

一、SpringSession

1.Session共享问题

1.1.Session原理

Session就好比是某个银行的一个用户的账户,底层实现就是一个key-value存储的Map,一个Session存储了用户很多的k-v信息,就像去招商银行(某台Session服务器)取钱,我们需要带上招商银行的银行卡(银行卡的账号对应一个JESSIONID),不需要带其他银行的银行卡,其他银行卡在招商银行不认识就。

1.2.分布式下session共享问题

  • 1.同一个服务,复制多份,session不同步问题
  • 2.不同服务(跨域),session不能共享问题

2.分布式Session共享问题解决方案

2.1.Session复制

最大的问题:占用大量的网络带宽,降低了服务的业务处理能力;水平扩展受限制,如1台服务1G的Session内存,总共有100台服务,每台服务都要另外保存99G的Session内存实现同步。所以该方案不可取。

2.2.客户端存储

问题:由于Session数据由浏览器来存储,每次请求都需要带上Session数据,存在很大安全隐患,并且每次请求携带session数据浪费了网络带宽。因此,此方案不会使用。

2.3.hash一致性

根据ip或sid计算hash实现负载均衡,保证了用户访问服务都是同一台机器,但也有可能机器宕机重启导致session失效,以及机器水平扩展重新hash导致session失效。但这些问题也不是很大,session本来是有效期的,所以这两种反向代理都可以使用。

2.4.统一存储

优点:服务器可以水平扩展,session没有安全隐患,服务重启或扩容都不会session丢失;不足的是需要增加一次网络调用,从redis获取数据比直接从内存读取慢很多;但这些不足都可以用SpringSession完美解决。

3.SpringSession整合

官方文档:https://docs.spring.io/spring-session/docs/2.1.0.BUILD-SNAPSHOT/reference/html5/guides/boot-redis.html

3.1.导入依赖

    implementation 'org.springframework.session:spring-session-data-redis'

3.2.配置Session存储方式

spring.session.store-type=redis

server.servlet.session.timeout=30m

3.3.启动类整合redis作为session存储

官方文档:https://docs.spring.io/spring-session/docs/2.1.0.BUILD-SNAPSHOT/reference/html5/#httpsession-redis-jc

@EnableRedisHttpSession //整合redis作为session存储

3.4.Controller使用Session

入参加上:HttpSession session
存储session信息:

        session.setAttribute("loginUser", data);

3.5.需要解决的问题

  • 1、默认发的令牌。session=N2ZjZWFjYmQtMGMzNi00Y2M2LWIwZjctZmIxMThiZTU5NzU3。作用域:当前域:(解决子域session共享问题)
  • 2、使用JSON的序列化方式来序列化对象数据到redis中

4.自定义SpringSession完成子域Session共享问题

4.1.SpringSession自定义Cookie

官方地址:https://docs.spring.io/spring-session/docs/2.1.0.BUILD-SNAPSHOT/reference/html5/guides/java-custom-cookie.html

代码配置:

@Configuration
public class GulimallSessionConfig {

    /**
     * Once you have setup Spring Session you can easily customize how the session cookie is written by exposing a
     * CookieSerializer as a Spring Bean. Out of the box, Spring Session comes with DefaultCookieSerializer. Simply
     * exposing the DefaultCookieSerializer as a Spring Bean will augment the existing configuration when using
     * configurations like @EnableRedisHttpSession.
     * (一旦您设置了Spring Session,就可以通过将CookieSerializer公开为Spring Bean来
     * 轻松定制会话cookie的编写方式。开箱即用,Spring Session附带了
     * DefaultCookieSerializer。当使用@EnableRedisHttpSession等配置时,简单地将
     * DefaultCookieSerializer公开为SpringBean将增加现有配置。)
     *
     * @return
     */
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setDomainName("gulimall.com"); //解决子域session共享问题
        return serializer;
    }
}

4.2.自定义SpringSession Redis序列化器

官方地址:https://docs.spring.io/spring-session/docs/2.1.0.BUILD-SNAPSHOT/reference/html5/#samples

代码配置:


    /**
     * 自定义SpringSession Redis序列化器
     * Custom RedisSerializer
     * You can customize the serialization by creating a Bean named springSessionDefaultRedisSerializer that
     * implements RedisSerializer<Object>.
     *
     * @return
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

5.SpringSession核心原理

核心原理:
@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
1、给容器中添加了一个组件
SessionRepository=>RedisIndexedSessionRepository:redis操作session。session的增删改查封装类
2、SessionRepositoryFilter=>Filter:session存储过滤器:每个请求过来都必须经过Filter
1)、创建的时候,就自动从容器中获取到SessionRepository;
2)、原生的request、response都被包装:SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper
3)、以后获取session。request.getSession()=>SessionRepositoryRequestWrapper
4)、wrappedRequest.getSession();==> SessionRepository中获取到的:sessionRepository.findById(sessionId)

使用了装饰者模式;
自动延期:redis中的数据也是有过期时间。

二、单点登录

1.单点登录流程(Redis+Cookie+令牌机制)

SpringSession只能解决同域名下的单点登录,但不能解决多系统(不同域名)下的单点登录问题

分布式单点登录框架参考:https://gitee.com/xuxueli0323/xxl-sso

1.1.单点登录流程图

1.2.单点登录流程-代码实现

1.2.1.单点登录的认证服务器sso-server

1.导入依赖:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2.配置文件application.properties

server.port=8080
spring.redis.host=192.168.56.10
spring.redis.port=6379

3.登录页login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
    用户名:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <input type="hidden" name="redirect_url" th:value="${url}"/>
    <input type="submit" value="登录">
</form>
</body>
</html>

4.LoginController

@Controller
public class LoginController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @ResponseBody
    @GetMapping("userInfo")
    public String userInfo(@RequestParam("token") String token) {
        String userInfo = redisTemplate.opsForValue().get(token);
        return userInfo;
    }

    @GetMapping("login.html")
    public String loginPage(@RequestParam("redirect_url") String url, Model model,
                            @CookieValue(value = "sso_token", required = false) String ssoToken) {
        if (!StringUtils.isEmpty(ssoToken)) {
            //说明之前有人登录过,浏览器留下了痕迹
            return "redirect:" + url + "?token=" + ssoToken;
        }
        model.addAttribute("url", url);

        return "login";
    }

    @PostMapping("doLogin")
    public String doLogin(@RequestParam("username") String username,
                          @RequestParam("password") String password,
                          @RequestParam("redirect_url") String url,
                          HttpServletResponse response) {

        if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
            //登录成功,跳回之前页面
            //把登录成功的用户存起来。
            String uuid = UUID.randomUUID().toString().replace("-", "");
            redisTemplate.opsForValue().set(uuid, username);
            Cookie ssoToken = new Cookie("sso_token", uuid);
            response.addCookie(ssoToken);
            return "redirect:" + url + "?token=" + uuid;
        }

        //登录失败,展示登录页
        return "login";
    }

}

1.2.2.单点登录的客户端sso-client

1.导入依赖:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2.配置文件application.properties

server.port=8081
sso.server=http://ssoserver.com:8080
sso.client=http://client1.com:8081

3.受访问保护的资源页employees.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>员工列表</title>
</head>
<body>
<h1>欢迎:[[${session.loginUser}]]</h1>
<ul>
    <li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>

4.HelloController

@Controller
public class HelloController {

    @Value("${sso.server}")
    private String ssoServerUrl;

    @Value("${sso.client}")
    private String ssoClientUrl;

    /**
     * 无需登录就可以访问
     *
     * @return
     */
    @ResponseBody
    @GetMapping("hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("employees")
    public String employees(Model model, HttpSession session,
                            @RequestParam(value = "token", required = false) String token) {
        if (!StringUtils.isEmpty(token)){
            //去ssoserver登录成功回来就会带上token
            //去ssoserver获取当前token真正对应的用户信息
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> forEntity = restTemplate.getForEntity(ssoServerUrl + "/userInfo?token=" + token, String.class);
            session.setAttribute("loginUser", forEntity.getBody());
        }

        Object loginUser = session.getAttribute("loginUser");
        if (loginUser == null) {
            //没登录,跳转到登陆服务器进行登录
            //跳转过去以后,使用url上的查询参数标识我们自己是哪个页面
            return "redirect:" + ssoServerUrl + "/login.html?redirect_url=" + ssoClientUrl + "/employees";
        }

        List<String> emps = new ArrayList<>();
        emps.add("张三");
        emps.add("李四");
        model.addAttribute("emps", emps);
        return "employees";
    }
}
posted @ 2022-10-31 17:21  冰枫丶  阅读(44)  评论(0编辑  收藏  举报