SpringBoot实现登录功能

1、前言

本文主要采用springboot+redis实现登录功能。持久层使用的是MyBatis Plus操作数据库。适合学完springboot,背过redis八股文的知识的实战练习。


效果图:

 


2、具体步骤

1.pom.xml :引入相关资源文件

<!-- redis数据 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- springboot web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>8.0.20</version>
        </dependency>

        <!-- lombok 实体注解-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- mybatis-plus 简化单表 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <!-- hutool 工具类 json处理 字符串 日期处理 随机数 ....-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

2.application.yaml: 配置文件

# 服务端口
server:
  port: 8081
spring:
  # 服务名称
  application:
    name: hmdp
  # mysql数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC&charactEncoding=UTF-8
    username: root
    password: root
  # redis数据源配置
  redis:
    host: 192.168.171.132  # 自己的虚拟机ip地址
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
  level:
    com.hmdp: debug  # 日志打印

3.基于session的验证码登录

1)发送验证码业务流程

  • controller/UserController
/**
     * 发送手机验证码
     */
    @PostMapping("/code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
 }
  • service/UserServiceImpl
//发送手机验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号 -- 正则表达式
        if (RegexUtils.isPhoneInvalid(phone)){
            //如果手机号不符合,就返回错误信息
            return Result.fail("手机号格式有误, 请重新输入!");
        }
        //符合,生成验证码  hutool-all工具包
        String code = RandomUtil.randomNumbers(6);

        //验证码保存到session中
        session.setAttribute("code", code);

        //发送验证码  -- 调用第三方技术的短信服务(参考阿里云视频点播的案例) 模拟发送短信成功  控制台打印日志模拟由短信服务上发送的验证码
        log.debug("短信验证码发送成功,【验证码】"+code);

        return Result.ok();
    }

2)用户登录业务流程

 注意:校验登录功能存在的问题:User对象存到session造成内存泄露

 解决:隐藏User对象的部分敏感属性

  • controller/UserController
/**
     * 登录功能
     * @RequestBody:{phone: "19956570011", code: "112211"}前端数据是json格式
     * 后端自动解析json格式的数据
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }
  • service/UserServiceImpl
//用户登录
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式有误, 请重新输入!");
        }
        //校验验证码
        Object codeCache= session.getAttribute("code");
        String code = loginForm.getCode();
        if (codeCache == null || !codeCache.toString().equals(code)){
            //验证码不一致,报错
            return Result.fail("验证码错误");
        }
        //一致,根据手机号查询用户user  select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        //判断用户是否存在
        if (user == null) {
            //不存在,创建新用户。根据手机号创建用户
            user = createUserWithPhone(phone);
        }
        //存在保存到session中
        session.setAttribute("user", user);
        return Result.ok();
    }

    private User createUserWithPhone(String phone) {
        //创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(6));
        //保存用户
        save(user);
        return user;
}

3)登录校验业务流程

 intercepter/LoginIntercepter 拦截器对象

public class LoginIntercepter implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从cookie获取session
        HttpSession session = request.getSession();
        //从session中获取User对象
        Object user = session.getAttribute("user");
        //判断:用户是否存在
        if (user == null){
            // 如果不存在,拦截  状态码:401  30x  40x  50x
            response.setStatus(401);
            return false;
        }
        // 存在用户保存到ThreadLocal中
        UserHolder.saveUser((UserDTO) user);

        //放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

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

}

config/MyMvcConfig  配置登录拦截器

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter())
                .excludePathPatterns(
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/user/code",
                        "/user/login"
                );
    }
}

controller/UserController

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

service/UserServiceImpl

//存在保存到session中 将user对象转为UserDto 避免传递敏感信息
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

4)用户退出登录

/**
     * 登出功能
     * @return 无
     */
    @PostMapping("/logout")
    public Result logout(HttpSession session){
        // 实现登出功能
        session.removeAttribute("user");
        return Result.ok("退出登录");
    }
  • 敏感信息(手机号、密码、身份证号、特殊时间点)传递的问题

  • 解决方案

若存在User对象,将User对象保存到session中
方法1:手动new UserDto对象封装id、nickName、icon属性
方法2:将user对象转为UserDto,BeanUtil.copyProperties(user, UserDTO.class避免传递敏感信息

基于session的登录存在的问题

session共享问题:用户请求的一个服务模块可能需要调用到服务器B,当用户发起请求的时候,此时的服务器B上并没有存储该用户的sessionID,所以就会再次让用户进行一个登陆操作。还有可能会导致用户本来就想完成一个下单操作,但是却还登陆了好几次的情况。

session不共享问题:请求切换到不同 服务器导致数据丢失。

4.基于redis共享session登录

  1. 将手机号生成的验证码保存到redis,以手机号作为key存储到redis,String类型的value是验证码。
  2. 验证码在redis设置过期时间。为了防止内存溢出。
  3. 用户对象保存到redis,以随机token作为key,value可以是String类型json字符串,可以hash类型。用token获取用户实现数据共享
  4. redis的用户生成token设置过期时间

controller/UserController

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

/**
     * 登录功能
     * @RequestBody:{phone: "19956570011", code: "112211"}前端数据是json格式
     * 后端自动解析json格式的数据
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

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

service/UserServiceImpl

//发送手机验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号 -- 正则表达式
        if (RegexUtils.isPhoneInvalid(phone)){
            //如果手机号不符合,就返回错误信息
            return Result.fail("手机号格式有误, 请重新输入!");
        }
        //符合,生成验证码  hutool-all工具包
        String code = RandomUtil.randomNumbers(6);

        //验证码保存到redis中 验证码code选择 String类型
        //设置 有效期 set key value ex
        //session.setAttribute("code", code);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //发送验证码  -- 调用第三方技术的短信服务(参考阿里云视频点播的案例) 模拟发送短信成功  控制台打印日志模拟由短信服务上发送的验证码
        log.debug("短信验证码发送成功,【" + code + "】");

        return Result.ok();
    }

    //用户登录
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式有误, 请重新输入!");
        }
        // 从redis获取验证码 校验验证码
        //Object codeCache= session.getAttribute("code");
        String codeCache = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (codeCache == null || !codeCache.toString().equals(code)){
            //验证码不一致,报错
            return Result.fail("验证码错误");
        }
        //一致,根据手机号查询用户user  select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        //判断用户是否存在
        if (user == null) {
            //不存在,创建新用户。根据手机号创建用户
            user = createUserWithPhone(phone);
        }
        //若存在User对象,将User对象保存到session中
        // 方法1:手动new UserDto对象封装id、nickName、icon属性
        // 方法2:将user对象转为UserDto,BeanUtil.copyProperties(user, UserDTO.class避免传递敏感信息
        //存在用户保存到redis
        //生成token
        String token = UUID.randomUUID().toString(true);
        //将User对象转换为hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //UserDTO对象属性保证String类型
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(false)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //存储
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);
        //设置token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);

        //返回token
        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        //创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(6));
        //保存用户
        save(user);
        return user;
    }

intercepter/LoginIntercepter 登录拦截器

private StringRedisTemplate stringRedisTemplate;

    public LoginIntercepter(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求头中获取token
        //HttpSession session = request.getSession();
        String token = request.getHeader("authorization");
        // 基于token获取redis的User对象
        //Object user = session.getAttribute("user");
        String  key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(key);
        //判断:用户是否存在
        if (userMap.isEmpty()){
            // 如果不存在,拦截  状态码:401  30x  40x  50x
            response.setStatus(401);
            return false;
        }
        // 将查询hash数据转换成UserDto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 存在用户保存到ThreadLocal中
        UserHolder.saveUser(userDTO);

        // 刷新token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        //放行
        return true;
    }

登录信息存储到redis效果图


彩蛋:谈谈对ThreadLocal以及应用的理解。(面试中的回答)

 1.实现线程间资源对象的线程隔离。

2. 实现线程内资源对象共享。

//新建一个用户资源的静态线程本地变量
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

tl.set(user);  //把获取到的资源存入到当前线程
tl.get();  //到当前线程去获取资源
tl.remove();  //删除当前线程关联的资源

ThreadLocalMap map = getMap(t) //当前线程内ThreadLocalMap 成员变量用来存储资源对象
private final int threadLocalHashCode = nextHashCode();//每次创建一个ThreadLocal对象nextHashCode计算资源对象存储的索引位置

ThreadLocalMap // 发生扩容时采用开放寻址法解决HashCode值冲突问题

//问题:ThreadLocalMap的key是为什么是弱引用?
static class Entry extends WeakReference<ThreadLocal<?>>
Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
//为了释放内存中的共享资源,线程池中线程长时间的运行占用的内存得不到释放导致内存不足GC需要回收不再使用的ThreadLocal的key。
1.get key 发现值是null, GC回收把key存到对应位置。
2.set key 使用启发式扫描,清除临近的key=null, 启发次数和元素个数是否和发现key=null有关。
3.ThreadLocal是静态成员变量是强引用,GC无法回收。调用remove

 至此完成系统登录功能,还有优化的地方欢迎大家在评论区留言!我们一起学习~~~

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Husp0707

你的小小点赞、关注是我的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值