> 文章列表 > 黑马的redis实战篇-短信登录

黑马的redis实战篇-短信登录

黑马的redis实战篇-短信登录

目录

四、实战篇-短信登录

4.1 导入黑马点评项目

1、后端:

2、前端

4.2 基于Session实现登录

1、发送验证码

2、短信验证码登录注册

3、校验登录状态

4.3 集群的session共享问题

4.4 基于Redis实现共享session登录

1、发送验证码

2、短信验证码登录注册

3、校验登录状态


四、实战篇-短信登录

4.1 导入黑马点评项目

项目的架构:

 

1、后端:

  • 导入项目后后端,创建数据库,加载sql

  • 修改配置文件

     

  • 启动

    启动后,访问http://localhost:8081/shop-type/list

     

2、前端

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

start nginx.exe

 

4.2 基于Session实现登录

1、发送验证码

 

@Overridepublic 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+phone,code);
​//5.发送验证码  真实是调用阿里云等平台短信服务的APIlog.info("发送验证码成功,验证码为: {}",code);
​//6.结束 返回okreturn Result.ok();}
​

2、短信验证码登录注册

 

@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号是否格式是否正确String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确");}
​//2.从session中取出验证码,校验验证码是否正确Object attribute = session.getAttribute(CODE+phone);
​if (attribute == null || !attribute.toString().equals(loginForm.getCode())) {//3.验证码不正确,直接返回return Result.fail("验证码不正确");}
​//4.通过,通过手机号查询用户是否存在LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getPhone,phone);User user = baseMapper.selectOne(lambdaQueryWrapper);
​//5.不存在进行注册if (user == null) {user = registerUser(phone);}
​//6.存在 保证用户到session中//注意这里需要进行脱敏UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);session.setAttribute(USER_INFO,userDTO);return Result.ok();}

3、校验登录状态

 

上面的逻辑,不光光是一个Controller要进行校验,其他Controller也需要校验,因此我们将其业务写入到拦截器中,如果有用户就放行,没有就不放行,然后在各自需要的Controller中获取用户,返回用户信息即可。如图所示

 

  • 创建拦截器类实现HandlerInterceptor,

    重写 preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

    和afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

    package com.hmdp.utils;
    ​
    import com.hmdp.dto.UserDTO;
    import org.springframework.web.servlet.HandlerInterceptor;
    ​
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    ​
    import static com.hmdp.utils.SystemConstants.USER_INFO;
    ​
    /* @packageName: com.hmdp.utils* @author: winter* @date: 2023/4/7 17:15* @version: 1.0* @email 1660420659@qq.com* @description: 拦截器*/
    public class LoginInterceptor implements HandlerInterceptor {/* 请求进入preHandle之前执行* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.通过session,获取用户Object attribute = request.getSession().getAttribute(USER_INFO);
    ​if (attribute == null) {//2.用户不存在,拦截器拦截response.setStatus(401);return false;}
    ​//3.将用户保证在ThreadLocal中  拦截器放行UserHolder.saveUser((UserDTO) attribute);return true;}
    ​
    ​/* 响应结束后,执行这个方法* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除 防止内存泄漏UserHolder.removeUser();}
    }
  • 配置Mvc配置类,拦截器的白名单

    package com.hmdp.config;
    ​
    import com.hmdp.utils.LoginInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    ​
    /* @packageName: com.hmdp.config* @author: winter* @date: 2023/4/7 17:39* @version: 1.0* @email 1660420659@qq.com* @description: mvc配置类*/
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    ​@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/").excludePathPatterns("/user/login","/user/code","/shop/","/shop-type/","/voucher/","/upload/","/blog/hot");}
    }
  • 将UserController中的me方法进行获取用户,返回信息

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

4.3 集群的session共享问题

session共享问题:当并发访问量多的时候,我们的解决办法可以说添加tomcat集群,但是就会出现问题

多态tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失问题

 

如图所示,因为使用nginx轮训方式,可能一个用户两次访问的tomcat并不是同一个,并且session不共享,会出现登录进去后,往往提示还需登录,用户的体验感极差,因此出现了解决方案,将数据都存储到redis集群中

4.4 基于Redis实现共享session登录

1、发送验证码

 

public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;@Resourceprivate StringRedisTemplate stringRedisTemplate;
​@Overridepublic Result sendCode(String phone, HttpSession session) {//1.验证手机号是否合法if(RegexUtils.isPhoneInvalid(phone)) {
​//2.如果不符合,返回错误信息return Result.fail("手机号不合法");}
​//3.符合,生成验证码String code = RandomUtil.randomNumbers(6);
​//4.将验证码保存到session中,并设置过期时间为2分钟stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
​//5.发送验证码log.info("发送验证码成功,验证码为: {}",code);
​//6.结束 返回okreturn Result.ok();}

2、短信验证码登录注册

 常量中的值

public static final String LOGIN_USER_KEY = "login:token:";
​
public static final Long LOGIN_USER_TTL = 36000L;
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号是否格式是否正确(其实这里应该也将手机号加入到session中,然手机号和验证码一一对应)String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确");}
​//2.从redsi中取出验证码,校验验证码是否正确String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
​if (cacheCode == null || !cacheCode.equals(loginForm.getCode())) {//3.验证码不正确,直接返回return Result.fail("验证码不正确");}
​//4.通过,通过手机号查询用户是否存在LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getPhone,phone);User user = baseMapper.selectOne(lambdaQueryWrapper);
​//5.不存在进行注册if (user == null) {user = registerUser(phone);}
​//6.存在 保证用户到redis中//6.1 注意这里需要进行脱敏UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
​//6.2 转换生成Map //这里因为使用的StringRedisTemplate 因此都是String//这里如果不装换成string  map中存储userDto对象的id是long类型//存储在redis的hash时 会出现String转换long 数字转换异常Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) ->fieldValue.toString()));
​//6.3.生成一个随机的tokenString token = UUID.fastUUID().toString(true);
​//6.4 存到到redis中String key = LOGIN_USER_KEY+token;stringRedisTemplate.opsForHash().putAll(key,map);//设置有效时间stringRedisTemplate.expire(key,LOGIN_USER_TTL,TimeUnit.MINUTES);return Result.ok(token);}
​/* 注册* @param phone* @return*/private User registerUser(String phone) {User user = new User();user.setPhone(phone);String nickName = RandomUtil.randomString(8);user.setNickName(USER_NICK_NAME_PREFIX+nickName);//保存用户到数据库baseMapper.insert(user);return user;}

3、校验登录状态

 

这里需要进行优化一下,需要使用两个拦截器,因为session的时候使用一个拦截器,只有登录等被拦截的请求,才会增加redis键值的时间,例如访问主页不经过拦截器,可能半小时后键值就自动销毁的,这样是不符合业务要求的,因此要在增加一个拦截器

 

  • 拦截器1 拦截一切路径

package com.hmdp.utils;
​
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
​
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
​
/* @packageName: com.hmdp.utils* @author: winter* @date: 2023/4/7 17:15* @version: 1.0* @email 1660420659@qq.com* @description: 拦截器所有请求*/
public class ExtraTimeInterceptor implements HandlerInterceptor {
​
​
​
​private StringRedisTemplate stringRedisTemplate;public ExtraTimeInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}
​/* 请求进入preHandle之前执行* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.通过请求获取到token信息String token = request.getHeader("authorization");//2.通过token从redis中获取对象信息Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);if (map.isEmpty()) {return  true;}//将map转换为对象UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
​//3.将用户保证在ThreadLocal中  拦截器放行UserHolder.saveUser( userDTO);
​//4.刷新用户存储有效期stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}
​
​/* 响应结束后,执行这个方法* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除 防止内存泄漏UserHolder.removeUser();}
​
}
​
  • 拦截器2 拦截部分需要登录才能访问的请求

package com.hmdp.utils;
​
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
​
/* @packageName: com.hmdp.utils* @author: winter* @date: 2023/4/7 17:15* @version: 1.0* @email 1660420659@qq.com* @description: 拦截部门请求,拦截需要登录的路径*/
public class LoginInterceptor implements HandlerInterceptor {
​
​
​/* 请求进入preHandle之前执行* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.取到用户UserDTO user = UserHolder.getUser();
​//2.判断用户是否为空if (user == null) {response.setStatus(401);return false;}return true;}
​
​
​
}

  • mvc配置一下拦截器需要拦截什么请求

package com.hmdp.config;
​
import com.hmdp.utils.ExtraTimeInterceptor;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
import javax.annotation.Resource;
​
/* @packageName: com.hmdp.config* @author: winter* @date: 2023/4/7 17:39* @version: 1.0* @email 1660420659@qq.com* @description: mvc配置类*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
​@Resourceprivate StringRedisTemplate stringRedisTemplate;
​@Overridepublic void addInterceptors(InterceptorRegistry registry) {//默认先添加执行  ,也可以设置order  值越小越先执行registry.addInterceptor(new ExtraTimeInterceptor(stringRedisTemplate)).addPathPatterns("/").order(0);registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/").excludePathPatterns("/user/login","/user/code","/shop/","/shop-type/","/voucher/","/upload/","/blog/hot").order(1);}
}
具体代码gitee上:

redis实战篇-hmdp-短信登录: 存放黑马点评中redis进行短信登录的代码 ,包括前端后后端