30天网站建设实录视频,网站运营,网页版微信手机版,程序员怎么做网站赚钱2023.12.6 短信登陆如果基于session来实现#xff0c;会存在session共享问题#xff1a;多台Tomcat不能共享session存储空间#xff0c;这会导致当请求切换到不同服务器时出现数据丢失的问题。 早期的解决办法是让session提供一个数据拷贝的功能#xff0c;即让各个Tomcat的…2023.12.6 短信登陆如果基于session来实现会存在session共享问题多台Tomcat不能共享session存储空间这会导致当请求切换到不同服务器时出现数据丢失的问题。 早期的解决办法是让session提供一个数据拷贝的功能即让各个Tomcat的数据实现一个互相的拷贝但是这种方式容易造成内存空间的浪费并且拷贝是需要时间的这段时间内依然存在数据不一致的问题。 于是我们可以基于Redis来完成由于Redis数据本身就是共享的就可以避免session共享的问题了并且redis是基于内存的读写性能高。 下面基于redis实现一下代码
发送短信验证码 之前是将验证码保存到session中现在是将验证码保存到redis中此时需要考虑redis采用什么数据结构来保存验证码key需要保证唯一性所以使用用户的手机号作为key由于验证码就是一个六位数的字符串所以value直接采用String保存就好。
该模块的代码如下 public Result sendCode(String phone, HttpSession session) {//1.校验手机号if(RegexUtils.isPhoneInvalid(phone)){//2.不符合则返回错误信息return Result.fail(手机号格式错误);}//3.符合生成验证码String code RandomUtil.randomNumbers(6);//保存验证码到session//session.setAttribute(code,code);//4.保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//设置过期时间定期清理redis中的验证码//5.发送验证码log.debug(发送验证码成功验证码{},code);return Result.ok();} 这里不真的发验证码就模拟一下就好了验证码直接输入到控制台上。
短信验证码登陆和注册模块 这里的redis要存的是用户对象信息所以value值我们采用HashMap结构去存该结构可以将对象的每个字段独立存储方便针对单个字段进行CRUD。 redis的key的选择这里不选择手机号而是采用一个随机token作为key此处的token最终是要返回给前端页面的类似于之前的session id(登陆凭证),所以key填手机号的话不安全并且为了后续能够通过这个token得到用户信息需要将此token返回给客户端。 该模块代码如下
Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号String phone loginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)){//2.不符合则返回错误信息return Result.fail(手机号格式错误);}//3.从redis获取验证码并校验String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone);String code loginForm.getCode();if(cacheCode null || !cacheCode.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);MapString, Object userMap BeanUtil.beanToMap(userDTO,new HashMap(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -fieldValue.toString()));//7.3 存储String tokenKey LOGIN_USER_KEY token;stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);//7.4 设置token有效期stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);// 返回tokenreturn 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;} 此处有个细节虽然我们设置了token有效期为30分钟但是不管我们登陆后做什么操作token始终都是30分钟后被消除。 而正常情况应该是我们登陆后如果有操作行为token的有效期应该被重置为30分钟。 于是我们可以使用一个拦截器对请求进行拦截每当有请求发生就将token的有效期重置。
拦截器的代码为
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token request.getHeader(authorization);if (StrUtil.isBlank(token)){return true;}//2.基于Token获取redis中的用户String key RedisConstants.LOGIN_USER_KEYtoken;MapObject, Object userMap stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY token);//3.判断用户是否存在if(userMap.isEmpty()){return true;}//5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6.保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.刷新token有效期stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
} 写完拦截器的代码之后还需要在MvcConfig中进行配置
Configuration
public class MvcConfig implements WebMvcConfigurer {Resourceprivate StringRedisTemplate stringRedisTemplate;Overridepublic void addInterceptors(InterceptorRegistry registry) {//登陆拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(/shop/**,/voucher/**,/shop-type/**,/upload/**,/blog/hot,/user/code,/user/login).order(1);//token刷新拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);}
} 这里虽然是在第二行配置的刷新有效期拦截器但是它会先进行拦截因为我们order里填的数字是0数字越小优先级越高。 第一行的拦截器是用来进行登陆校验的即如果你访问了除代码中路径以外的路径的话需要判断你有没有登陆那如何判断你有没有登陆呢只需要查看当前ThreadLocal中有无用户信息就知道了该拦截器代码如下
public class LoginInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否需要拦截(即ThreadLocal中是否有用户)if(UserHolder.getUser() null){response.setStatus(401);return false;}return true;}} 拦截器的整体架构图如下