【黑马点评实战篇|第一篇:基于Redis实现登录】

基于Session实现登录流程

实现发送短信验证码功能

在点评的登录页面有一个发送验证码,打开发现请求为post,路径如下

实现流程

在实现代码时找到UserController

调用service方法

然后实现业务如下,检验手机号->利用hutool工具包的工具类生成6位数的数字验证码,

java 复制代码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误");
        }
        //生成验证码
        String num = RandomUtil.randomNumbers(6);
        session.setAttribute("code",num);
        log.info("发送验证码成功,{}",num);
        return Result.ok();
    }

session.setAttribute()设置session

实现登录拦截:编写拦截器

拦截器流程:

编写配置类,让拦截器生效

隐藏用户敏感信息

我们注意到在用user给前端返回数据时有很多无用数据,比如password,我们就再次创建了一个DTO来封装我们需要的信息,来保护敏感信息

封装成UserDTO来返回,

由于需要返回DTO,所以相关的类里返回值也需要返回DTO

就会只返回部分信息了

Session共享问题:

同一个用户若是访问不同服务器,就等于访问不同的tomcat,JSESSIONID 对应的 Session 对象和数据,在不同 Tomcat 中是独立且无关联的,在A服务器中存放的数据,不能拿到B服务器使用,就会导致数据不一致。

很容易就可以想到redis完美的对应了上述要求

基于Redis实现短信登录

第一部分:短信验证码登录 、注册:

以手机号为key读取验证码

先修改发送验证码的内容:将验证码存入redis

1.注入StringRedisTemplate,调用方法得到redis,进行校验

2.保存用户到redis(设计key和value,因为value是对象我们用Hash存储)

2.1key用uuid生成随机token

2.2把查询到的user封装成map

2.3存入redis
这里用StringRedisTemplate不用RedisTemplaye是因为前者有自带的序列化器能够将数据序列化字符串

保存用户进入redis
java 复制代码
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误");
        }
        //TODO 用redis校验验证码
        String code = loginForm.getCode();
        String codeInSession = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        if (codeInSession == null || !codeInSession.equals(code)){
            return Result.fail("验证码错误");
        }
        //根据手机号查询用户
        User user = query().eq("phone", phone).one();
        if (user == null){
            user = createUserWith(phone);
        }
        //保存用户信息到redis
        //随机token保存数据作为key,用户信息保存数据作为value
        String token = UUID.randomUUID().toString(true);
        //把用户信息存储转为map
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                new CopyOptions()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((key, value) -> value.toString()));
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);
        stringRedisTemplate.expire(LOGIN_USER_KEY+token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return Result.ok(token);

这里beantoMap方法需要自定义转化时map的泛型

复制代码
new CopyOptions()作用:创建一个 CopyOptions 对象,用于定义转换过程中的各种选项。
CopyOptions 是 Hutool 工具库提供的类,用于精细化控制 Bean 到 Map 的转换行为。
复制代码
setIgnoreNullValue(true)作用:设置是否忽略值为 null 的字段。
如果某个字段的值是 null,则不会将其放入结果 Map 中。
复制代码
setFieldValueEditor((key, value) -> value.toString()));作用:对字段值进行自定义编辑。
参数说明:
key:当前字段的名称(String 类型)。
value:当前字段的值(Object 类型)。
逻辑:将所有字段值统一转换为字符串形式(调用 toString() 方法)。
意义:确保 Map 中的所有值都是 String 类型,避免后续操作中因类型不匹配而出现问题(例如存储到 Redis 时)。

如果只传入一个DTO不进行转换设计,就会出现类型转换问题,如Long转换成String报错,这里因为DTO中id是Long类型Map中的value是String类型

第二部分:校验登录状态

因为注册器中有@Configuration注解,所以它在Spring的Bean容器中就可以直接注入

java 复制代码
    @Resource
    private StringRedisTemplate stringRedisTemplate;

而拦截器里没有,就需要注册器传递一个StringRedisTemplate对象给拦截器,拦截器才能在方法里使用StringRedisTemplate

注册器注入StringRedisTemplate

拦截器注入

注意,因为获取用户时根据key传入时,返回的是value里面有很多键值对,所以用entries()方法

java 复制代码
        //1.得到authorization请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank( token)){
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        //2.基于token获取用户
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        //3.判断用户是否存在
        if(entries.isEmpty()){
              //4.不存在,拦截,返回401状态码
              response.setStatus(401);
              return false;
        }
        //存在,保存用户信息,需要把map转为DTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
//        5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //6.放行
        return true;
    }

用户刷新状态:

目标:用户每一次操作都刷新token有效时长,不然用户中途需要重新验证

思路:把上面写的短信验证等拦截操作放入一个新的拦截器,拦截所有的路径请求,保证每一次都成功刷新,原本的拦截器只需要查询用户是否在redis存在即可

java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser() == null){
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        return true;
    }
}
相关推荐
我待_JAVA_如初恋2 小时前
Redis常用的数据类型之String
数据库·redis·缓存
@ chen2 小时前
MySQL 中的锁机制
数据库·mysql
Elastic 中国社区官方博客2 小时前
Elasticsearch:使用 Elastic Workflows 构建自动化
大数据·数据库·人工智能·elasticsearch·搜索引擎·自动化·全文检索
数智工坊2 小时前
【数据结构-栈】3.1栈的顺序存储-链式存储
java·开发语言·数据结构
短剑重铸之日2 小时前
《设计模式》第七篇:适配器模式
java·后端·设计模式·适配器模式
OnYoung2 小时前
编写一个Python脚本自动下载壁纸
jvm·数据库·python
Apple_羊先森2 小时前
ORACLE数据库巡检SQL脚本--15、表空间的运行状态
数据库·sql·oracle
R-G-B2 小时前
python 验证每次操作图片处理的顺序是否一致,按序号打上标签,图片重命名
开发语言·python·图片重命名·按序号打上标签·验证图片处理的顺序
小二·2 小时前
Go 语言系统编程与云原生开发实战(第10篇)性能调优实战:Profiling × 内存优化 × 高并发压测(万级 QPS 实录)
开发语言·云原生·golang