Redis实现分布式会话

Redis实现分布式会话

1 什么是分布式会话

1 这是我么之前学过的注册登录模式

2 如果非常多的人访问,因为单台服务器的访问承受能力是有限的,那么我们就想用多态服务器来承担压力

3 一般通过负载均衡的方式来实现,来分担服务器的压力。

4 负载均衡解释。

官方解释: 网络专用术语,负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。

大白话:nginx就是一个接受请求,然后决定请求最终那个服务器来接受,这个算法我们后面给大家讲nginx或者ribbon的时候给大家补充,但是有时候会存在这样的问题,用户1第一次请求到tomcat1, 下一次请求的时候就可能请求到tomcat2了,这样会存在session丢失,然后系统提示我们需要登录。

5 解决方案。

  • session 复制,也就是当一个服务器有新的session保存的时候,通过服务器通信机制,然后将session复制到其他的服务器,如果服务器较多的话,会存在大量的网路和io占用,效率低下。

  • redis实现session共享。

2 准备条件

1 导入资料中的代码

注意修改mysql和redis的地址

访问端口:http://localhost:8081/shop-type/list 如果有数据显示,说明项目部署成功。

2 导入前端代码

3 启动代码

在nginx所在目录下打开一个CMD窗口,输入命令:start nginx

输入http://127.0.0.1:8080

3 验证码

1 redis序列化配置

md-end-block 复制代码
package com.xinzhi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建Template
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // key和 hashKey采用 string序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        // value和 hashValue采用 JSON序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        return redisTemplate;
    }
}

2 controller

md-end-block 复制代码
 @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
 }

3 service

md-end-block 复制代码
public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);
}
md-end-block 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1 校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
        //2 如果不符合返回错误消息
            return Result.fail("手机号格式错误");
        }
        // 3 生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 4 保存验证码到session
        session.setAttribute("code",code);
        // 5 发送验证码,发送短信验证大家添加
        log.debug("验证码发送成功:"+code);
        return Result.ok();
    }
}

验证码功能已经实现。

4 登录

1 controller

md-end-block 复制代码
 /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm,HttpSession session){
        return userService.login(loginForm,session);
    }

2 service

md-end-block 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    //1 校验手机号
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号码格式错误");
    }
    // 2 从session中获取code并校验
    Object cacheCode = session.getAttribute("code");
    if(cacheCode==null || !cacheCode.toString().equals(loginForm.getCode())){
        return Result.fail("验证码错误");
    }
    // 3 根据手机号查找用户
    User user = query().eq("phone", phone).one();
    // 4 用户不存在则创建用户
    if(user==null){
        user = createUserByPhone(phone);
    }
    // 5 用户保存到session
    session.setAttribute("user", user);
    return Result.ok();
}
private User createUserByPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName("xinzhi_" + RandomUtil.randomString(8));
    save(user);
    return user;
}

3 创建intercepter包,创建拦截器

md-end-block 复制代码
package com.xinzhi.intercepter;

import com.xinzhi.entity.User;
import com.xinzhi.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


public class LoginIntercepter implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1 获取session
        HttpSession session = request.getSession();
        // 2 从session中获取user对象
        Object user = session.getAttribute("user");
        // 3 判断session中时候有对象
        if(user==null){
            // 4 不存在的话,设置401状态
            response.setStatus(401);
            return false;
        }
        //5 存在的话保存到threadlocal中
        UserHolder.saveUser((User)user);
        //6 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

4 将拦截器添加到 WebMvcConfigurer 中

WebMvcConfigurer是可以添加自定义拦截器,消息转换器等 。

md-end-block 复制代码
package com.xinzhi.config;


import com.xinzhi.intercepter.LoginIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop-type/**",
                        "/shop/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
    }
}

5 user/me处理

前端验证完成以后,要跳转到user/me,因为被拦截了

md-end-block 复制代码
@GetMapping("/me")
    public Result me(){
        User user = UserHolder.getUser();
        return Result.ok(user);
}

5 简单的反向代理

1 反向代理主要是修改nginx的配置文件

2 idea设置端口启动

同一个项目启动两个端口,参考:IDEA中使用--server.port=端口号启动多个SpringBoot项目实例_在idea中server怎么有port-CSDN博客

3 发送短信验证码

4 结果我们虽然能输入正确的验证码,但是还是不能登录。这是因为session不一致导致的。

6 token

Token是在服务端产生的,如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回token给前端,前端可以在每次请求的时候带上token证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

参考:什么是Token(令牌)-CSDN博客

使用步骤:

  • 通过用户名和密码登录,验证通过以后,服务器会生成一个token(本质就是一个字符串),并且把token保存起来。

  • 服务器会通过响应的方式,将token返回给前端。

  • 下次浏览器访问客户端的时候,就会带着token一起过来,并且和服务器的token对比,如果相同则登录成功。

7 redis实现session共享

8 验证码改造

1 将验证码从之前的保存到session中,改到保存到redis中,因为存在多个用户登录的情况,为了方便区分验证码是哪个手机发出的,所以保存验证码的时候,键可以用带有手机号的标志来保存。并且指定失效时间,发短信的时候可以提示用户验证码有效期。

md-end-block 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    //1 校验手机号
    if(RegexUtils.isPhoneInvalid(phone)){
        //2 如果不符合返回错误消息
        return Result.fail("手机号格式错误");
    }
    // 3 生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 4 保存验证码到session
    //session.setAttribute("code",code);
    // 4 保存验证码到redis
    redisTemplate.opsForValue().set("login.code:"+phone, code,15,TimeUnit.MINUTES);
    // 5 发送验证码
    log.debug("验证码发送成功:"+code);
    return Result.ok();
}

9 登录改造

1 之前是从session中获取验证码,现在验证码保存到redis中了,所以验证码需要从redis获取

2 之前用户信息是保存到session中的,现在需要保存到redis中。使用hash的方式,但是user的属性比较多,可以用map的方式直接保存到redis的hash结构中。

3 可以使用BeanUtil工具类将对象转成map类型。

4 因为需要给前端发送token,所以需要随机生成一个token

md-end-block 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    //1 校验手机号
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号码格式错误");
    }
    // 2 从session中获取code并校验
    //Object cacheCode = session.getAttribute("code");
    // 2 从redis中获取code并校验
    Object cacheCode = redisTemplate.opsForValue().get("login.code:" + phone);
    System.out.println(cacheCode);
    if(cacheCode==null || !cacheCode.toString().equals(loginForm.getCode())){
        return Result.fail("验证码错误");
    }
    // 3 根据手机号查找用户
    User user = query().eq("phone", phone).one();
    // 4 用户不存在则创建用户
    if(user==null){
        user = createUserByPhone(phone);
    }
    // 5 用户保存到session
    // session.setAttribute("user", user);
    // 5 用户保存到redis
    //5.1生成token值
    String token = UUID.randomUUID().toString(true);
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
    //5.2 将用户信息保存到token中
    redisTemplate.opsForHash().putAll("login.token:"+token,userMap);
    // 5.3 设置token的过期时间
    redisTemplate.expire("login.token:" + token, 7, TimeUnit.DAYS);
    //6 将token返回给前端
    return Result.ok(token);
}
private User createUserByPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName("xinzhi_" + RandomUtil.randomString(8));
    save(user);
    return user;
}

5 前端以后访问的时候,在请求头里面带上了token

6 拦截器获取前端

md-end-block 复制代码
package com.xinzhi.intercepter;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.xinzhi.entity.User;
import com.xinzhi.utils.UserHolder;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;


public class LoginIntercepter implements HandlerInterceptor {

    private RedisTemplate<String, Object> redisTemplate;

    public LoginIntercepter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1 获取session
        // HttpSession session = request.getSession();
        // 1 从请求头中获取token
        String token = request.getHeader("authorization");
        // 2 如果前端没有带token,直接给失败的响应
        if(StrUtil.isBlank(token)){
            response.setStatus(401);
            return false;
        }
        // 2 从session中获取user对象
        //Object user = session.getAttribute("user");
        // 3  获取redis中的用户对象
        String key = "login.token:" + token;
        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
        // 4 判断redis中的对象
        if(userMap.isEmpty()){
            // 5 不存在的话,设置401状态
            response.setStatus(401);
            return false;
        }
        System.out.println("拦截器中的user:"+userMap);
        // 6 将userMap转成user对象
        User user = BeanUtil.fillBeanWithMap(userMap, new User(), true);
        //7 存在的话保存到threadlocal中
        UserHolder.saveUser(user);
        //8 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

10 可以同时访问两个服务器。

相关推荐
wefg15 分钟前
【Redis】安装与启动
数据库·redis·缓存
大圣编蚕30 分钟前
常见的 Redis 面试题(高频整理)
数据库·redis·缓存
派小心.1 小时前
网站统计采样数据的误差核对
大数据·服务器·前端·数据库·人工智能·小程序·数据分析
龙腾AI白云1 小时前
数字孪生、具身智能、世界模型三者交叉
数据库·人工智能·机器学习·flask·pygame
小猿姐1 小时前
KubeBlocks Enterprise v2.3 发布!
redis·kubernetes·aiops
SelectDB技术团队2 小时前
Apache Doris 基于 Lance 的向量检索深度解析
数据库·数据仓库·数据分析·湖仓一体·apache doris·lakehouse·多模湖仓
donk13142 小时前
React Native for OpenHarmony 实战:三方库 react-native-step-indicator 的鸿蒙化适配指南
分布式·react native·react.js·wpf·harmonyos
这个DBA有点耶2 小时前
从INDEX到JOIN_ORDER:MySQL 8.0 Hint语法体系与五类实战场景
数据库·mysql·架构
数字新视界2 小时前
机房动环监控助力智能管理
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
梅菲塔亚2 小时前
博客_Redis入门到实战
数据库·redis·缓存