Redis实战—验证码登录功能实现

7 篇文章 0 订阅
2 篇文章 0 订阅

  本博客为个人学习笔记,学习网站:黑马程序员Redis入门到实战 实战篇之验证码登录

目录

基于Session实现登录流程

发送短信验证码

短信验证码登录、注册

校验登录状态

session共享问题

Redis代替session的业务流程

设计Key的结构

设置Key的细节

整体访问流程

代码

解决状态登录刷新问题


基于Session实现登录流程

发送短信验证码
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号;如果手机号合法,后台此时生成对应的验证码,同时将验证码保存到session中,然后再通过短信的方式将验证码发送给用户。

短信验证码登录、注册
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息。

校验登录状态
用户在请求时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行 。


发送短信验证码

Controller层代码

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

Service层代码

//Service接口类代码
public interface IUserService extends IService<User> {
    Result sendCode(String phone, HttpSession session);
}

//Service接口实现类代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号码格式错误!");
        }
        // 3.如果符合,生成随机6位验证码
        String code = RandomUtil.randomNumbers(6);
        // 4.保存验证码到session
        session.setAttribute("code", code);
        // 5.模拟发送验证码到手机(控制台)
        log.debug("你收到的黑马点评短信验证码为:" + code);
        return Result.ok();
    }
}

短信验证码登录、注册

如果我们在信息传输时使用User类,通过浏览器可以观察到此时用户的全部信息都被暴露,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了。

UserDTO类代码

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

Controller层代码

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

@GetMapping("/me")
public Result me() {
    // 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

Service层代码

//Service接口类方法代码
Result login(LoginFormDTO loginForm, HttpSession session);

//Service接口实现类代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号码格式错误!");
    }
    // 2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.toString().equals(code)) {
        // 3.验证码不一致则报错
        return Result.fail("验证码错误");
    }

    // 4.一致 -> 根据手机号查询用户
    User user = query().eq("phone", phone).one();
    // 5.判断用户是否存在
    if (user == null) {
        // 6.若用户不存在 -> 创建新用户
        user = createUserWithPhone(phone);
    }
    // 7.将user转换为UserDTO类,保存用户信息到session中
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
    return Result.ok();
}

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

校验登录状态

通过拦截器实现登录校验功能


拦截器类代码

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户
        Object user = session.getAttribute("user");
        // 3.判断用户是否存在
        if (user == null) {
            // 4.若用户不存在则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.若用户存在则保存用户信息到 ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        // 6.放行
        return true;
    }

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

 拦截器配置类代码

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                    //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

session共享问题

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?

早期的方案是session拷贝,虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session被修改时,都会同步给其他Tomcat服务器的session,这样的话,就可以实现session的共享了。

但是这种方案具有两个大问题
1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟

所以后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了


Redis代替session的业务流程

设计Key的结构


首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。


设置Key的细节

我们可以使用String结构,就是一个简单的key,value键值对的方式。但是关于key的处理,session的特性是每个用户都拥有自己的session,各个用户之间的session相互独立,互不干扰。但redis的key是共享的,为避免干扰,我们不能使用code(验证码)来作为key了,我们需要找到一个能唯一标识各个用户的key。

在设计这个key的时候,我们之前讲过需要满足两点
1、key要具有唯一性
2、key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了。


整体访问流程

当注册完成后,用户登录时会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。


代码

接口参数修改,去掉session参数

public interface IUserService extends IService<User> {

    Result sendCode(String phone);

    Result login(LoginFormDTO loginForm);
}

修改Service接口实现类代码,将验证码保存到redis并设置有效时间为2min

@Override
public Result sendCode(String phone) {
    // 1.校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号码格式错误!");
    }
    // 3.如果符合,生成随机6位验证码
    String code = RandomUtil.randomNumbers(6);
    // 4.保存验证码到redis并设置有效时间   // set key value ex 120
    stringRedisTemplate.opsForValue().set("login:code" + phone, code, 2, TimeUnit.MINUTES);
    // 5.模拟发送验证码到手机(控制台)
    log.debug("你收到的黑马点评短信验证码为:" + code);
    return Result.ok();
}

修改Service接口实现类登录代码,

    @Override
    public Result login(LoginFormDTO loginForm) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            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)) {
            // 3.验证码不一致则报错
            return Result.fail("验证码错误");
        }

        // 4.一致 -> 根据手机号查询用户
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在
        if (user == null) {
            // 6.若用户不存在 -> 创建新用户
            user = createUserWithPhone(phone);
        }
        // 7.保存用户信息到redis中
        // 7.1 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2 将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        // 7.3 存储
        stringRedisTemplate.opsForHash().putAll("login:token:" + token, userMap);
        // 7.4 设置有效期
        stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
        // 8 返回token
        return Result.ok(token);
    }

修改拦截器代码

注意
由于该LoginInterceptor的对象是我们手动创建,手动new出来的,并不是通过spring创建的,因此StringRedisTemplate对象的注入不能采用@Autowire,@Resource等方式,只有spring/springboot帮我们创建的对象才可以使用@Autowire,@resource

public class LoginInterceptor implements HandlerInterceptor {

    /*
     * 注意!!!
     * 由于该LoginInterceptor的对象是我们手动创建,手动new出来的,并不是通过spring创建的
     * 因此StringRedisTemplate对象的注入不能采用@Autowire,@Resource等方式
     * 只有spring/springboot帮我们创建的对象才可以使用@Autowire,@resource
     * */
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(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)) {
            // 用户不存在则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3.判断用户是否存在(entries如果获取到一个为null的Map时,会返回一个空Map,因此下面的if判断只需要判断该Map是否为空就行)
        if (userMap.isEmpty()) {
            // 4.若用户不存在则拦截,返回401状态码
            response.setStatus(401);
            System.err.println("不放行!");
            return false;
        }
        // 5.将查询到的Hash数据转换为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.若用户存在则保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.SECONDS);
        // 8.放行
        System.err.println("放行!");
        return true;
    }

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

拦截器配置类代码

注意

在LoginInterceptor类中的注释中,我们说到:由于LoginInterceptor类并不是通过spring创建的,因此不能通过注解的形式注入StringRedisTemplate类,只能通过构造函数添加。
而配置类MvcConfig是通过注解@Configuration由spring创建的,因此可以用注解的形式注入StringRedisTemplate类。

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /*
     * 在LoginInterceptor类中的注释中,我们说到
     * 由于LoginInterceptor类并不是通过spring创建的,因此不能通过注解的形式注入StringRedisTemplate类,只能通过构造函数添加
     * 而配置类MvcConfig是通过注解@Configuration由spring创建的,因此可以用注解的形式注入StringRedisTemplate类
     * */

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

再次运行,输入手机号获取验证码,填写验证码点击登录后出现报错,内容如下图
报错:显示Long类型无法转换为String类型

 修改接口实现类中,login方法中的代码


解决状态登录刷新问题

由于我们设置的拦截器只能拦截有关登录请求,使得其他无关登录的请求无法被拦截,将导致部分请求无法及时更新token的有效期,因此我们再设置一个拦截器,用来专门拦截所有请求,更新token的有效期。

新增拦截器代码如下:

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)) {
            // 该拦截器只负责更新数据有效期,因此如果token值为空,则直接放行,由后面的拦截器处理
            return true;
        }
        // 2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3.判断用户是否存在(entries如果获取到一个为null的Map时,会返回一个空Map,因此下面的if判断只需要判断该Map是否为空就行)
        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(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.SECONDS);
        // 8.放行
        System.err.println("放行!");
        return true;
    }

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

修改原来的拦截器代码
由于新增的拦截器在获取请求头中的token,并基于token获取到redis中的用户时,会将该用户转换为UserDTO对象,并存储到UserHolder中,因此原来的拦截器不必再做多余的解析token操作,只需要根据UserHolder中的User是否存在来判断用户的登录状态并决定是否放行即可。

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截 (ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            //不存在用户,需要拦截
            response.setStatus(401);
            return false;
        }
        // 有用户则放行
        return true;
    }
}

修改拦截器配置类:添加新拦截器,令其拦截所有请求,同时为两个拦截器设置order(order越小则越先执行)

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是Spring Boot Redis验证码登录功能的基本步骤: 1. 首先在Spring Boot项目中添加依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> ``` 2. 创建一个Redis配置类,并配置Redis连接信息: ```java @Configuration public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379)); } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } } ``` 3. 创建一个用户实体类,并添加相应的字段: ```java @Data @AllArgsConstructor @NoArgsConstructor public class User { private String username; private String password; private String email; private String verificationCode; } ``` 4. 创建一个验证码登录控制器,并在其中添加验证码生成和校验逻辑: ```java @RestController public class VerificationCodeLoginController { private final RedisTemplate<String, Object> redisTemplate; public VerificationCodeLoginController(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, @RequestParam String verificationCode) { User user = new User(username, password, null, verificationCode); Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Set<ConstraintViolation<User>> violations = validator.validate(user); if (!violations.isEmpty()) { return "Invalid user information."; } // Get the verification code from Redis. String key = "verification_code:" + username; String storedVerificationCode = (String) redisTemplate.opsForValue().get(key); if (storedVerificationCode == null || !storedVerificationCode.equals(verificationCode)) { return "Invalid verification code."; } // Remove the verification code from Redis. redisTemplate.delete(key); // Log the user in. // ... return "Login successful."; } @GetMapping("/verification-code") public String getVerificationCode(@RequestParam String username) { // Generate a random verification code. String verificationCode = RandomStringUtils.randomNumeric(6); // Save the verification code to Redis. String key = "verification_code:" + username; redisTemplate.opsForValue().set(key, verificationCode, 5, TimeUnit.MINUTES); // Return the verification code. return verificationCode; } } ``` 在以上代码中,`login()`方法用于校验用户名、密码和验证码,并根据校验结果执行登录逻辑。在校验过程中,它首先从Redis中获取该用户的验证码,并与传入的验证码进行比较。如果两者不一致,则返回“Invalid verification code.”,否则从Redis中删除验证码,并执行登录逻辑。`getVerificationCode()`方法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值