黑马点评-01基于Redis实现短信登陆的功能

环境准备

当前模型

nginx服务器的作用

  • 手机或者app端向nginx服务器发起请求,nginx基于七层模型走的是HTTP协议,可以实现基于Lua直接绕开tomcat访问Redis

  • nginx也可以作为静态资源服务器,轻松扛下上万并发并负载均衡到下游的tomcat服务器,利用集群支撑起整个项目

  • 使用nginx部署前端项目后还可以做到动静分离,进一步降低tomcat服务的压力

企业级MySQL加上固态硬盘能够支撑的并发大概就是4000起~7000左右,对于上万的并发如果让tomcat直接访问Mysql,瞬间会让Mysql服务器的cpu和硬盘全部打满

  • 在高并发场景下需要选择使用MySQL集群,同时为了进一步降低MySQL的压力增加访问的性能,一般还需要使用Redis集群

导入数据/前端/后端

执行项目所需要的SQL脚本,MySQL的版本要采用5.7及以上版本,否则执行脚本时部分SQL语句无法执行

说明
tb_user 用户表
tb_user_info 用户详情表
tb_shop 商户信息表
tb_shop_type 商户类型表
tb_blog 用户日记表(达人探店日记)
tb_follow 用户关注表
tb_voucher 优惠券表
tb_voucher_order 优惠券的订单表

导入后端项目,将项目放到你的idea工作空间,然后利用idea打开即可

  • 第一步: 修改application.yaml文件中的MySQL和Reids的连接地址为自己服务所在的地址
  • 第二步: 启动项目,在浏览器访问http://localhost:8081/shop-type/list,如果可以看到JSON数据则说明导入成功

导入前端工程,将nginx的解压目录放到一个自己指定的目录(确保目录不含中文,特殊字符和空格)

  • 第一步: 在ngnix所在目录打开一个CMD窗口,执行start nginx.exe命令启动ngnix服务
  • 第二步: 打开浏览器并将页面调整为手机模式,访问http://localhost:8080/访问项目首页(页面的数据是从后端查询得到的)
properties 复制代码
server {
        listen       9999;
        server_name  localhost;
        # 指定前端项目所在的位置
        location / {
            root   html/hmdp;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

		# 监听的路径
        location /api {  
            default_type  application/json;
            #internal;  
            keepalive_timeout   30s;  
            keepalive_requests  1000;  
            #支持keep-alive  
            proxy_http_version 1.1;  
            rewrite /api(/.*) $1 break;  
            proxy_pass_request_headers on;
            #more_clear_input_headers Accept-Encoding;  
            proxy_next_upstream error timeout;  
	     	# 反向代理的位置
          	proxy_pass http://127.0.0.1:8081;
            #proxy_pass http://backend;
        }
    }

短信登录

基于Session实现登录流程

实现发送短信验证码功能

用户点击发生验证码按钮发起请求

UserController中的sendCode方法处理发生验证码请求,在UserServiceImpl编写具体的业务逻辑

  • 使用邮箱验证时我们还需要去数据库中修改phone的字段类型,将varchar(11)改为varchar(100)
java 复制代码
/**
*发送手机验证码
*/	
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    return userService.sendcode(phone,session);
}
java 复制代码
/**
* 发送手机验证码
*/
@Override
public Result sendcode(String phone, HttpSession session) {
    // 1.校验手机号,RegexUtils是我们创建的工具类,里面还需要用到RegexPatterns工具类
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.符合生成验证码,RandomUtil是hutool-all的工具类 
    String code = RandomUtil.randomNumbers(6);
    // 4.保存验证码到session
    session.setAttribute("code",code);
    // 5.发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);
    // 6.返回成功的信息
    return Result.ok();
}

/**
* 发送邮箱验证码
*/
@Override
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) throws MessagingException {
    // TODO 发送短信验证码并保存验证码
    if (RegexUtils.isEmailInvalid(phone)) {
        return Result.fail("邮箱格式不正确");
    }
    String code = MailUtils.achieveCode();
    session.setAttribute(phone, code);
    log.info("发送登录验证码:{}", code);
    MailUtils.sendTestMail(phone, code);
    return Result.ok();
}

实现登录功能

用户点击登录按钮发起请求

java 复制代码
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    return userService.login(loginForm, session);
}
java 复制代码
// loginForm中封装了登录参数,包含手机号、验证码或者手机号、密码
@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}
java 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号的格式
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.将从session中获取的验证码与用户提交的验证码进行比较
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    // 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
    if(cacheCode == null || !cacheCode.toString().equals(code)){
        // 4.验证码不一致则报错
        return Result.fail("验证码错误");
    }
    // 5.验证码一致则根据用户提交的手机号取数据库中查询用户
    User user = query().eq("phone", phone).one();

    // 判断用户是否存在
    if(user == null){
        //6.不存在则创建
        user =  createUserWithPhone(phone);
    }
    //7.将用户的信息保存到session中
    session.setAttribute("user",user);
	// 返回成功的信息
    return Result.ok();
}

// 创建一个新用户
private User createUserWithPhone(String phone) {
    //创建用户
    User user = new User();
    //设置手机号
    user.setPhone(phone);
    //设置昵称(默认名),一个固定前缀+随机字符串,USER_NICK_NAME_PREFIX是工具类中的系统常量
    user.setNickName(USER_NICK_NAME_PREFIX+ RandomUtil.randomString(8));
    //将用户信息保存到数据库
    save(user);
    return user;
}

实现登录验证功能(拦截器)

当我们登录成功后,前端还会发起一个user/me的请求用于登录校验,由于访问每个Controller都需要登录校验,所以我们可以把校验流程写在拦截器中

第一步; 定义一个工具类UserHolder专门用来存储用户的信息,而且每个线程都对应一个自己的用户信息

java 复制代码
public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();
    public static void saveUser(User user){
        // 保存用户的信息,默认的key就是当前线程
        tl.set(user);
    }
    public static User getUser(){
        // 获取用户信息,默认的key就是当前线程
        return tl.get();
    }
    public static void removeUser(){
        // 删除用户信息,默认的key就是当前线程
        tl.remove();
    }
}

第二步: 创建一个LoginInterceptor类实现HandlerInterceptor接口,防止用户直接通过url路径访问项目的功能,重写前置拦截器方法和完成处理方法

  • preHandle方法: 用于我们登陆之前的权限校验,同时将从session中获取的用户信息保存到UserHolder的ThreadLocal中,方便以后在Controller中获取用户信息
  • afterCompletion方法: 用于处理登录后的信息避免内存泄露
Java 复制代码
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.用户存在则将用户信息保存到UserHolder的Threadlocal中
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws 			Exception {
        UserHolder.removeUser();
    }
}

第三步:编写配置类,注册登录拦截器并设置该拦截器需要拦截的请求

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册登录拦截器使其生效
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(// 设置拦截器不需要拦截的请求
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

第四步: 获取当前线程对应的登录用户信息并响应到前端完成登录校验

java 复制代码
@GetMapping("/me")
public Result me() {
    User user = UserHolder.getUser();
    return Result.ok(user);
}

隐藏用户敏感信息

在进行登录校验时将登录用户的全部信息响应给浏览器的行为是不安全的,所以我们应当在返回登录用户信息之前将用户的敏感信息进行隐藏

json 复制代码
{
    "success":true,
    "data":{
        "id":1010,
        "phone":"1586385296",
        "password":"",
        "nickName":"user_i1b3ir09",
        "icon":"",
        "createTime":"2022-10-22T14:20:33",
        "updateTime":"2022-10-22T14:20:33"
    }
}

第一步: 新建一个不含用户敏感信息UserDto对象,在进行登录校验时返回的含有用户敏感信息的User对象转化成UserDto对象

java 复制代码
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

第二步: 修改UserHolder工具类中ThreadLocal的泛型

java 复制代码
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

第三步: 修改login方法中,将保存到session域中的User对象转换成UserDto对象

java 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号的格式
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.将从session中获取的验证码与用户提交的验证码进行比较
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    // 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
    if(cacheCode == null || !cacheCode.toString().equals(code)){
        // 4.验证码不一致则报错
        return Result.fail("验证码错误");
    }
    // 5.验证码一致则根据用户提交的手机号取数据库中查询用户
    User user = query().eq("phone", phone).one();

    // 判断用户是否存在
    if(user == null){
        //6.不存在则创建
        user =  createUserWithPhone(phone);
    }
    //7.将用户的信息隐藏后再存到到session中
    session.setAttribute("user",user);
    //UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);      
	//session.setAttribute("user", userDTO);
	session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
    // 返回成功的信息
    return Result.ok();
    
}

第四步: 修改拦截器中将从session中获取的隐藏了用户信息的UserDTO对象保存到UserHolder类的ThreadLocal中

java 复制代码
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中隐藏了用户信息的UserDTO类型的用户对象,并保存到UserHolder类的ThreadLocal中
        UserDTO user = (UserDTO) session.getAttribute("user");
        UserHolder.saveUser(user);        
        //3.判断用户是否存在
        if(user == null){
              //4.不存在则拦截当前请求并返回401状态码
              response.setStatus(401);
              return false;
        }
        //5.若存在保存用户的隐藏信息到Threadlocal
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
}

第五步: 修改登录校验的方法,将UserHolder中的ThreadLocal保存的UserDTO对象返回

java 复制代码
// 修改获取的类型为用户的隐藏信息
@GetMapping("/me")
public Result me() {
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

第六步: 重启服务器,登录校验后返回的用户信息已经不含敏感信息

json 复制代码
{
    "success":true,
    "data":{
        "id":1016,
        "nickName":"user_zkhf7cfv",
        "icon":""
    }
}
相关推荐
SimonKing几秒前
手搓MCP客户端动态调用多MCP服务,调用哪个你说了算!
java·后端·程序员
白仑色9 分钟前
Redis 如何保证数据安全?
数据库·redis·缓存·集群·主从复制·哨兵·redis 管理工具
孤独得猿10 分钟前
Redis类型之Hash
redis·算法·哈希算法
写bug写bug16 分钟前
分布式锁的使用场景和常见实现(上)
分布式·后端·面试
Ali酱24 分钟前
2周斩获远程offer!我的高效求职秘诀全公开
前端·后端·面试
小韩博26 分钟前
网络安全(Java语言)简单脚本汇总 (一)
java·安全·web安全
浩浩测试一下1 小时前
02高级语言逻辑结构到汇编语言之逻辑结构转换 if (...) {...} else {...} 结构
汇编·数据结构·数据库·redis·安全·网络安全·缓存
青云交1 小时前
飞算 JavaAI 深度实战:从老项目重构到全栈开发的降本增效密码
java·代码生成·全栈开发·效率提升·智能编程·老项目重构·飞算 javaai
TinpeaV1 小时前
(JAVA)自建应用调用企业微信API接口,实现消息推送
java·redis·企业微信·springboot·springflux