Evaluate项目开发日志(上)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
日志服务 SLS,月写入数据量 50GB 1个月
简介: Evaluate项目开发日志(上)

黑马点评项目的学习日志



项目需要实现的功能介绍

image-20230218101124109.png


项目架构image-20230218101531022.png


前端登录 ,使用nginx启动前端项目 ,然后访问8080端口,必须是在后端项目启动的情况下


数据转换


Bean --- > String : Bean.toString()

String ----> Bean : Shop shop = JSONUtil.toBean(shopJson, Shop.class);

Bean ---->hashMap : Map<String, Object> map1 = BeanUtil.beanToMap(userDTO);

hash ---->Bean : UserDTO userDTO1 = BeanUtil.mapToBean(map, UserDTO.class, true);


2023.2.18—–短信登录



基于session实现发送验证码登录


流程图的分析


发送短信验证码

image-20230218111418914.png


接口:@PostMapping("/user/code")


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


service层实现流程


@Override
public Result sendCode(String phone, HttpSession session) {
// TODO 实现发送验证码方法
    /**
     * 1. 校验手机号是否合格
     * 2. 不合格怎么做
     //合格..........
     * 1. 生成验证码
     * 2. 保存验证码到 session
     * 3. 发送验证码
     */
    //1. 校验手机号是否合格(一般使用正则表达式去校验) 这里我们封装到RefexUtils.isPhoneInvalid是否是无效手机号
    boolean isNotNumber = RegexUtils.isPhoneInvalid(phone);
    //不是手机号
    if(isNotNumber){
        return Result.fail("手机号格式错误 !!!");
    }
    //合格,是手机号
    //1. 生成验证码
    String code = RandomUtil.randomNumbers(6);
    session.setAttribute("code",code);
    log.debug("发送短信验证码成功 !");    //需要调用阿里云的测试,暂时不是重点 ,无需实现
    //返回ok就行了
    return Result.ok();
}


短信验证码登录

image-20230218111427520.png


  1. 提交验证码和手机号,并且进行判断是否正确
  2. 正确 : 就继续 ,错误 : 返回验证码错误
  3. 调用数据库查询用户是否存在
  4. 存在的话 : 保存用户信息到session,不存在 : 就跳转到注册页面 ,注册并保存到数据库


接口 : @PostMapping("/user/login")


@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // TODO 实现登录功能
    return  userService.login(loginForm,session);
}


service层实现

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // TODO 实现登录功能
    //1. 提交验证码和手机号,并且进行判断是否正确
    String phone = (String) session.getAttribute("phone");
    String code = (String) session.getAttribute("code");
    //2. **正确 : **就继续 ,**错误**  : 返回验证码错误
    if(!loginForm.getPhone().equals(phone) || !loginForm.getCode().equals(code)){
        return Result.fail("手机号/验证码错误!");
    }
    //3. 调用数据库查询用户是否存在
    User user = query().eq("phone", phone).one();
    //4. **存在的话** : 保存用户信息到session,**不存在** : 就跳转到注册页面 ,注册并保存到数据库
    if(user == null){
        user = createUserWithPhone(phone);
    }
    //保存
    session.setAttribute("user",user);
    return Result.ok();
}
private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    //生成随机的用户名
    user.setNickName("user_" + RandomUtil.randomString(10));
    return user;
}

校验登录状态

image-20230218131635655.png


在拦截器中是实现校验功能


流程


  1. 获取请求携带的cookie
  2. 获取用户
  3. 判断用户是否存在,存在 : 保存该线程 ,**不存在 :**拦截


首先实现拦截器

在拦截器中我们就可以实现我们需要的登录流程


package com.hmdp.config.Handler;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * 配置登录校验拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 拦截请求
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求携带的cookie
        HttpSession session = request.getSession();
        //2. 获取用户
        Object user =  session.getAttribute("user");
        //3. 判断用户是否存在,**存在 :** 保存该线程 ,**不存在 :**拦截
        if(user == null){
            response.setStatus(401);
            return false;
        }
        //4. 保存信息到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        //放行
        return true;
    }
    /**
     * 销毁用户的信息,避免信息的泄露 ,防止内存泄露
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户 ,清除的是线程中的
        UserHolder.removeUser();
    }
}


在mvc配置类中添加拦截器


package com.hmdp.config;
import com.hmdp.config.Handler.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
        //排除不需要拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

登录


@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // TODO 实现登录功能
    //1. 提交验证码和手机号,并且进行判断是否正确
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号格式错误!");
    }
    String code = (String) session.getAttribute("code");
    //2. **正确 : **就继续 ,**错误**  : 返回验证码错误
    if(phone == null || !loginForm.getCode().equals(code)){
        return Result.fail("手机号/验证码错误!");
    }
    //3. 调用数据库查询用户是否存在
    User user = query().eq("phone", phone).one();
    //4. **存在的话** : 保存用户信息到session,**不存在** : 就跳转到注册页面 ,注册并保存到数据库
    if(user == null){
        user = createUserWithPhone( phone);
    }
    //保存
    /**
     * 因为这样会将所有的用户的信息都传过来,这样不利于保护用户隐私 ,所以我们映射到前端的时候不能将全部的信息都返回
     * 仅仅返回一些简单的信息即可,所以我们就用到了UserDTO
     * 使用Bean.copyProperties就可以将user中的属性的值拷贝一份给userDto
     * 然后存储到session的就是我们的UserDTo对象
     */
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    session.setAttribute("user",userDTO);
    return Result.ok();
}


对于返回的信息,因为我们登录时会设置密码等 ,一系列隐私属性,如果我们返回前端这些属性的话,那么势必会造成信息泄露。所以为例用户安全着想 我们映射到前端的时候不能将全部的信息都返回 。 仅仅返回一些简单的信息即可,所以我们就用到了UserDTO 。 使用Bean.copyProperties就可以将user中的属性的值拷贝一份给userDto。 然后存储到session的就是我们的UserDTo对象, 这样就避免用户信息在传入前端时出现信息泄露的风险。


获取当前用户并返回


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


集群的Session共享问题



存储的信息是在单线程中的,所以多台Tomcat并不能共享session的存储空间 ,当请求切换到不同的tomcat服务器导致数据丢失的问题 ——session共享问题


image-20230218145140010.png


当用户第一次进入系统时,tomcat服务器①接收到请求,然后进行处理用户的请求(登录注册等)。


当用户第二次进入系统时 ,被负载均衡到了tomca服务器② 。用户的信息其实是已经注册了的,但是这里却无法获取。导致用户还得注册…这会造成用户体验感很差,所以我们需要继续处理。


这就是Session共享的问题


解决办法:


实现session共享。

Redis实现解决Session共享问题


使用redis代替session解决。


基于Redis实现共享session登录


image-20230218151559751.png

1.当我们实现生成验证码时 ,就可以直接将以 【手机号为 key : 验证码为value】 保存到reids中

2.输入完验证码,点击登录的时候。进行校验,我们就可以在redis中查询当前手机号保存的value与输入的进行比较。如果比较正确就下一步

3.根据手机号查询是否有这个用户。如果有就保存这个用户的信息到redis ,【以随机的token为key : 用户信息为value】 。

4.如果没有查到,那么就注册新的用户。然后保存到数据库(mysql),然后再回到步骤3进行


image-20230218151620667.png

5.校验登录状态时, 请求就会携带着token。


6.然后我们就可以通过携带得token得value属性值判断用户是否存在。如果存在,那么就保存用户到ThreadLocal中,然后放行该请求。


7.如果不存在,那么拦截器就会拦截请求


实现


1.发送短信验证码,然后将验证码保存到redis中

//1. 生成验证码
String code = RandomUtil.randomNumbers(6);
/**-----------------
         * 保存验证码到redis中
         * 添加业务前缀
         * 设置验证码的有效期2分钟
         */
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private UserMapper userMapper;
@Override
public Result sendCode(String phone, HttpSession session) {
    // TODO 实现发送验证码方法
    /**
         * 1. 校验手机号是否合格
         * 2. 不合格怎么做
         //合格..........
         * 1. 生成验证码
         * 2. 保存验证码到 session
         * 3. 发送验证码
         */
    //1. 校验手机号是否合格(一般使用正则表达式去校验) 这里我们封装到RefexUtils.isPhoneInvalid是否是无效手机号
    boolean isNotNumber = RegexUtils.isPhoneInvalid(phone);
    //不是手机号
    if(isNotNumber){
        return Result.fail("手机号格式错误 !!!");
    }
    //合格,是手机号
    //1. 生成验证码
    String code = RandomUtil.randomNumbers(6);
    /**-----------------
         * 保存验证码到redis中
         * 添加业务前缀
         * 设置验证码的有效期 2分钟
         */
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
    //-----------------
    log.debug("发送短信验证码成功 ! 验证码为  : [" + code + "]");    //需要调用阿里云的测试,暂时不是重点 ,无需实现
    //返回ok就行了
    return Result.ok();
}

2.点击登录的时候。进行校验,我们就可以在redis中查询当前手机号保存的value与输入的进行比较


@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // TODO 实现登录功能
    //1. 提交验证码和手机号,并且进行判断是否正确
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号格式错误!");
    }
    String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    //2. **正确 : **就继续 ,**错误**  : 返回验证码错误
    if(phone == null || !loginForm.getCode().equals(code)){
        return Result.fail("验证码错误!");
    }
    //3. 调用数据库查询用户是否存在
    User user = query().eq("phone", phone).one();
    //4. **存在的话** : 保存用户信息到session,**不存在** : 就跳转到注册页面 ,注册并保存到数据库
    if(user == null){
        user = createUserWithPhone( phone);
    }
    //保存
    /**
     * 1.保存用户信息到 redis中
     * 2. 随机生成token, 作为登录令牌
     * 3. 将user对象转成hashmap去存储
     * 4. 存储
     * 5. 返回token
     */
    String token = UUID.randomUUID().toString(true);
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create().setIgnoreNullValue(true).
                    setFieldValueEditor((filedName , fieldValue) -> fieldValue.toString()));  //将对象转成map
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);
    //存储完成设置有效期 30 min
    stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL ,TimeUnit.MINUTES);
    /**
     * 如果用户30分钟内一直进行访问的话,那么有效期就会不断的变化,所以我么就需要再拦截器中设置,一旦用户点击,就是有了请求
     * 那么就重置30分钟,一直往复的设值,那么就实现了用户30分钟不点点击就删除token的设置
     */
    return Result.ok(token);
}

重点1


//保存
/**
     * 1.保存用户信息到 redis中
     * 2. 随机生成token, 作为登录令牌
     * 3. 将user对象转成hashmap去存储
     * 4. 存储
     * 5. 返回token
     */
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
  //3. 将user对象转成hashmap去存储
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),                                             CopyOptions.create().setIgnoreNullValue(true)
    .setFieldValueEditor((filedName , fieldValue) -> fieldValue.toString())); 
//上面那样写的目的是为了实现bean转换成map,因为我们的redis中的hash结构key 和 value全都是String类型,而在UserDTO中,id属性为Long类型,无法无法强转为String类型,所以就需要我们自定以hash中的存储类型为Long类型
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);
//存储完成设置有效期 30 min
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL ,TimeUnit.MINUTES);
/**
     * 如果用户30分钟内一直进行访问的话,那么有效期就会不断的变化,所以我么就需要再拦截器中设置,一旦用户点击,就是有了请求
     * 那么就重置30分钟,一直往复的设值,那么就实现了用户30分钟不点点击就删除token的设置
     */


重点2

重置token的时常,实现30分钟为点击删除token 以及**一旦点击某个请求就重置token的时间**


方法: 新增一个拦截器,只处理点击请求就重置token时间的问题


image-20230218164320306.png


两个不同作用的拦截器

package com.hmdp.config.Handler;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
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;
/**
 * 点击某个请求, 就重置token时间的拦截器
 */
public class preHandler implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public preHandler(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求携带的token
        String token = request.getHeader("authorization");
        String key = LOGIN_USER_KEY+ token;
        //2. 基于token获取redis中用户
        //3. 判断用户是否存在 ,如果不存在直接放行,不进行下面的步骤
        if(StrUtil.isBlank(token)){
            return true;
        }
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        //将redis查询到的用户信息hashmap转换成user对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        //4. 保存信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(key,LOGIN_USER_TTL , TimeUnit.MINUTES);
        //放行
        return true;
    }
}


package com.hmdp.config.Handler;
import com.hmdp.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 配置登录校验拦截器
 */
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;
    }
}

MVC配置文件中设置拦截器的执行顺序


通过后面的order(x); //x数字越小优先级越高,越先执行

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
        //排除不需要拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
        //先执行
        //拦截所有的请求 ,作用就是用户登录了就点击刷新token消失的时间
        registry.addInterceptor(new preHandler(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
机器学习/深度学习 SQL 存储
人工智能平台PAI 操作报错合集之机器学习PAI训练的时候logging.info('Train and evaluate finish')后, 总会报出来一个错如何解决
阿里云人工智能平台PAI (Platform for Artificial Intelligence) 是阿里云推出的一套全面、易用的机器学习和深度学习平台,旨在帮助企业、开发者和数据科学家快速构建、训练、部署和管理人工智能模型。在使用阿里云人工智能平台PAI进行操作时,可能会遇到各种类型的错误。以下列举了一些常见的报错情况及其可能的原因和解决方法。
|
6月前
|
XML 移动开发 开发工具
使用markdown,knitr和pandoc在R语言中编写可重现的报告
使用markdown,knitr和pandoc在R语言中编写可重现的报告
|
存储 缓存 JSON
Evaluate项目开发日志(中)
Evaluate项目开发日志(中)
92 0
|
NoSQL 安全 前端开发
Evaluate项目开发日志(下)
Evaluate项目开发日志(下)
58 0
|
安全 fastjson Unix
《IntelliJ IDEA 插件开发》第七节:通过Inspection机制,对静态代码安全审查
一、前言 二、需求目的 三、案例开发 1. 工程结构 2. 伪随机数检测 3. FastJson检测 4. 提醒模板 5. 检测配置 四、测试验证 五、总结 六、系列推荐
1181 0
《IntelliJ IDEA 插件开发》第七节:通过Inspection机制,对静态代码安全审查
|
存储 数据可视化 计算机视觉
目标检测的Tricks | 【Trick10】工具类文件调用(coco评价指标包、日志工具、Tensorboard工具...)
目标检测的Tricks | 【Trick10】工具类文件调用(coco评价指标包、日志工具、Tensorboard工具...)
714 0
目标检测的Tricks | 【Trick10】工具类文件调用(coco评价指标包、日志工具、Tensorboard工具...)
|
PyTorch 算法框架/工具
PyG (PyTorch Geometric) Dropbox系图数据集无法下载的解决方案(AMiner, DBLP, IMDB, LastFM)(持续更新ing...)
本文主要关注PyG官方提供的数据集中,因为数据下载源是Dropbox,所以不能直接下载的解决方案。
【pytest官方文档】解读fixtures - 11. fixture的执行顺序,3要素详解(长文预警)
【pytest官方文档】解读fixtures - 11. fixture的执行顺序,3要素详解(长文预警)
【pytest官方文档】解读fixtures - 11. fixture的执行顺序,3要素详解(长文预警)
|
前端开发
前端工作总结137-webstorm持续updating indices解决方法
前端工作总结137-webstorm持续updating indices解决方法
186 0
|
前端开发
#yyds干货盘点# 【React工作记录四十三】ant design From实时刷新
#yyds干货盘点# 【React工作记录四十三】ant design From实时刷新
111 0
#yyds干货盘点# 【React工作记录四十三】ant design From实时刷新