前言

前面写了基于Session实现登录,我们都知道这种传统的会话技术,在现在的企业开发当中是不是会存在很多的问题:

  • 服务器集群环境下无法直接使用Session
  • 移动端APP(Android、IOS)中无法使用Cookie
  • 用户可以自己禁用Cookie
  • Cookie不能跨域(Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了)

来看看基于Redis实现登陆界面

来看看二者流程图对比:
在这里插入图片描述

在这里插入图片描述

来看看登陆界面的代码实现(我把Session的实现代码没删掉都注释起来了):

UserController类 :

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // TODO 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

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


IUserService 接口:

package com.hmdp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;

import javax.servlet.http.HttpSession;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);
    Result login(LoginFormDTO loginForm, HttpSession session);

}

IUserService 接口实现类:

package com.hmdp.service.impl;


import

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @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
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //5.发送验证码
        System.out.println("发送短信验证码成功,验证码:" + code);

        //5.返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }

        //2.校验验证码 不一致直接报错 从session中获取
//        Object cachecode = session.getAttribute("code");
//        String code = loginForm.getCode();
//        if (cachecode == null || !cachecode.toString().equals(code)) {
//            return Result.fail("验证码错误!");
//        }

        //2.校验验证码 从redis中获取  不一致直接报错
        String cachecode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cachecode == null || !cachecode.toString().equals(code)) {
            return Result.fail("验证码错误!");
        }


        //3.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        //4.判断用户是否存在
        if (user == null) {
            //5.不存在,创建新用户
            user = createUserWithPhone(phone);

        }

        //6.存在,保存用户信息到session
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

        //6.存在,保存用户信息到redis

        //6.1 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);

        //6.2 将用户对象转为hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue( true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

        //6.3 存储
        String tokenkey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll( tokenkey, userMap);
        //6.4设置token有效期
        stringRedisTemplate.expire( tokenkey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        //6.4返回token

        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;

    }

}

拦截器:

package com.hmdp.utils;

import ...

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //1.获取请求头中的 token
        String token = request.getHeader("authorization");
        if (token == null || token.isEmpty()) {
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        //2.基于token获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);


        //3.判断用户是否存在
        if (userMap.isEmpty()) {
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        //4.存在,将查询到的Hash数据转为UserDTO对象,保存用户信息到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser( userDTO );

        //6.刷新 token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        //7.放行
        return true;
    }

}

🧩 Redis 知识点全面解析(基于代码)

一、Redis 数据结构的使用
  1. String 类型:用于存储验证码
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
  • 数据结构:String
  • Key:login:code:13812345678 //手机号码为Key
  • Value:“123456”(验证码)
  • TTL:设置过期时间
  1. Hash 类型:用于存储用户登录信息
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
  • 数据结构:Hash
  • Key:login:token:abc123-def456…
  • Value:{ “id”: “1”, “phone”: “138…”, “nickName”: “user_xxx” }
  1. Key 命名建议使用冒号分隔(namespace 风格)
package com.hmdp.utils;

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

}

  1. Token 设置过期时间
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 例如 30 分钟

实现“自动退出”功能。
避免用户长期登录占用内存。

二、Bean → Map 转换与 Hash 存储
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
    CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())
);

这行代码的作用是:

将一个 Java 对象(userDTO)转换为一个 Map<String, Object>,并进行自定义处理:忽略 null 值字段,并将所有字段值转为字符串。

这里也用到了Hutool

本文是学习黑马程序员—黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

…。。。。。。。。。。。…请添加图片描述

…。。。。。。。。。。。…

Logo

网易易盾是国内领先的数字内容风控服务商,依托网易二十余年的先进技术和一线实践经验沉淀,为客户提供专业可靠的安全服务,涵盖内容安全、业务安全、应用安全、安全专家服务四大领域,全方位保障客户业务合规、稳健和安全运营。

更多推荐