【尚庭公寓SpringBoot + Vue 项目实战】登录管理(十八)

【尚庭公寓SpringBoot + Vue 项目实战】登录管理(十八)


文章目录

      • 【尚庭公寓SpringBoot + Vue 项目实战】登录管理(十八)
        • 1、登录业务介绍
        • 2、接口开发
          • 2.1、获取图形验证码
          • 2.2、登录接口
          • 2.3、获取登录用户个人信息

1、登录业务介绍

登录管理共需三个接口,分别是获取图形验证码登录获取登录用户个人信息,除此之外,我们还需为所有受保护的接口增加验证JWT合法性的逻辑,这一功能可通过HandlerInterceptor来实现。后台管理系统的登录流程如下图所示

image-20240619221355636

2、接口开发
2.1、获取图形验证码

查看接口

image-20240619221628521

代码开发

  • 查看响应的数据结构

    查看web-admin模块下的com.atguigu.lease.web.admin.vo.login.CaptchaVo,内容如下

    @Data
    @Schema(description = "图像验证码")
    @AllArgsConstructor
    public class CaptchaVo {@Schema(description="验证码图片信息")private String image;@Schema(description="验证码key")private String key;
    }
    
  • 配置所需依赖

    • 验证码生成工具

      本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档。

      common模块的pom.xml文件中增加如下内容

      <dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId>
      </dependency>
      
    • Redis

      common模块的pom.xml中增加如下内容

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

      application.yml中增加如下配置

      spring:data:redis:host: <hostname>port: <port>password:<password>database: 0
      

      注意:上述hostnamepasswordport需根据实际情况进行修改,,如果你redis没有密码,可以省略

  • 编写Controller层逻辑

    LoginController中增加如下内容

    @Operation(summary = "获取图形验证码")
    @GetMapping("login/captcha")
    public Result<CaptchaVo> getCaptcha() {CaptchaVo captcha = service.getCaptcha();return Result.ok(captcha);
    }
    
  • 编写Service层逻辑

    • LoginService中增加如下内容

      CaptchaVo getCaptcha();
      
    • LoginServiceImpl中增加如下内容

      @Autowired
      private StringRedisTemplate redisTemplate;@Override
      public CaptchaVo getCaptcha() {SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);specCaptcha.setCharType(Captcha.TYPE_DEFAULT);String code = specCaptcha.text().toLowerCase();String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID();String image = specCaptcha.toBase64();redisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS);return new CaptchaVo(image, key);
      }
      

      知识点

      • 本项目Reids中的key需遵循以下命名规范:项目名:功能模块名:其他,例如admin:login:123456

      • spring-boot-starter-data-redis已经完成了StringRedisTemplate的自动配置,我们直接注入即可。

      • 为方便管理,可以将Reids相关的一些值定义为常量,例如key的前缀、TTL时长,内容如下。大家可将这些常量统一定义在common模块下的com.atguigu.lease.common.constant.RedisConstant类中

        public class RedisConstant {public static final String ADMIN_LOGIN_PREFIX = "admin:login:";public static final Integer ADMIN_LOGIN_CAPTCHA_TTL_SEC = 60;public static final String APP_LOGIN_PREFIX = "app:login:";public static final Integer APP_LOGIN_CODE_RESEND_TIME_SEC = 60;public static final Integer APP_LOGIN_CODE_TTL_SEC = 60 * 10;public static final String APP_ROOM_PREFIX = "app:room:";
        }
        
2.2、登录接口

查看接口

image-20240619221948522

登录校验逻辑

用户登录的校验逻辑分为三个主要步骤,分别是校验验证码校验用户状态校验密码,具体逻辑如下

  • 前端发送usernamepasswordcaptchaKeycaptchaCode请求登录。
  • 判断captchaCode是否为空,若为空,则直接响应验证码为空;若不为空进行下一步判断。
  • 根据captchaKey从Redis中查询之前保存的code,若查询出来的code为空,则直接响应验证码已过期;若不为空进行下一步判断。
  • 比较captchaCodecode,若不相同,则直接响应验证码不正确;若相同则进行下一步判断。
  • 根据username查询数据库,若查询结果为空,则直接响应账号不存在;若不为空则进行下一步判断。
  • 查看用户状态,判断是否被禁用,若禁用,则直接响应账号被禁;若未被禁用,则进行下一步判断。
  • 比对password和数据库中查询的密码,若不一致,则直接响应账号或密码错误,若一致则进行入最后一步。
  • 创建JWT,并响应给浏览器。

代码开发

  • 查看请求数据结构

    查看web-admin模块下的com.atguigu.lease.web.admin.vo.login.LoginVo,具体内容如下

    @Data
    @Schema(description = "后台管理系统登录信息")
    public class LoginVo {@Schema(description="用户名")private String username;@Schema(description="密码")private String password;@Schema(description="验证码key")private String captchaKey;@Schema(description="验证码code")private String captchaCode;
    }
    
  • 配置所需依赖

    登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT,配置如下,具体内容可参考官方文档。

    • 引入Maven依赖

      common模块的pom.xml文件中增加如下内容

      <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId>
      </dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime</scope>
      </dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><scope>runtime</scope>
      </dependency>
      
    • 创建JWT工具类

      common模块下创建com.atguigu.lease.common.utils.JwtUtil工具类,内容如下

      public class JwtUtil {private static long tokenExpiration = 60 * 60 * 1000L;private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());public static String createToken(Long userId, String username) {String token = Jwts.builder().setSubject("USER_INFO").setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).claim("userId", userId).claim("username", username).signWith(tokenSignKey).compact();return token;}
      }
      
  • 编写Controller层逻辑

    LoginController中增加如下内容

    @Operation(summary = "登录")
    @PostMapping("login")
    public Result<String> login(@RequestBody LoginVo loginVo) {String token = service.login(loginVo);return Result.ok(token);
    }
    
  • 编写Service层逻辑

    • LoginService中增加如下内容

      String login(LoginVo loginVo);
      
    • LoginServiceImpl中增加如下内容

      @Override
      public String login(LoginVo loginVo) {//1.判断是否输入了验证码if (!StringUtils.hasText(loginVo.getCaptchaCode())) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);}//2.校验验证码String code = redisTemplate.opsForValue().get(loginVo.getCaptchaKey());if (code == null) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);}if (!code.equals(loginVo.getCaptchaCode().toLowerCase())) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);}//3.校验用户是否存在SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername());if (systemUser == null) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);}//4.校验用户是否被禁if (systemUser.getStatus() == BaseStatus.DISABLE) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);}//5.校验用户密码if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);}//6.创建并返回TOKENreturn JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
      }
      
  • 编写Mapper层逻辑

    • LoginMapper中增加如下内容

      SystemUser selectOneByUsername(String username);
      
    • LoginMapper.xml中增加如下内容

      <select id="selectOneByUsername" resultType="com.atguigu.lease.model.entity.SystemUser">select id,username,password,name,type,phone,avatar_url,additional_info,post_id,statusfrom system_userwhere is_deleted = 0and username = #{username}
      </select>
      
  • 编写HandlerInterceptor

    我们需要为所有受保护的接口增加校验JWT合法性的逻辑。具体实现如下

    • JwtUtil中增加parseToken方法,内容如下

      public static Claims parseToken(String token){if (token==null){throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);}try{JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();return jwtParser.parseClaimsJws(token).getBody();}catch (ExpiredJwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);}catch (JwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);}
      }
      
    • 编写HandlerInterceptor

      web-admin模块中创建com.atguigu.lease.web.admin.custom.interceptor.AuthenticationInterceptor类,内容如下,有关HanderInterceptor的相关内容,可参考官方文档。

      @Component
      public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("access-token");JwtUtil.parseToken(token);return true;}
      }
      

      注意

      我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为access-token

    • 注册HandlerInterceptor

      web-admin模块com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration中增加如下内容

      @Autowired
      private AuthenticationInterceptor authenticationInterceptor;@Override
      public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");
      }
      
  • Knife4j配置

    在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中,如下图所示。

    image-20240619222213976

    注意:每个接口分组需要单独配置,刷新页面,任选一个接口进行调试,会发现发送请求时会自动携带该header

2.3、获取登录用户个人信息

查看接口

image-20240619222327267

代码开发

  • 查看请求和响应的数据结构

    • 响应的数据结构

      查看web-admin模块下的com.atguigu.lease.web.admin.vo.system.user.SystemUserInfoVo,内容如下

      @Schema(description = "员工基本信息")
      @Data
      public class SystemUserInfoVo {@Schema(description = "用户姓名")private String name;@Schema(description = "用户头像")private String avatarUrl;
      }
      
    • 请求的数据结构

      按理说,前端若想获取当前登录用户的个人信息,需要传递当前用户的id到后端进行查询。但是由于请求中携带的JWT中就包含了当前登录用户的id,故请求个人信息时,就无需再传递id

  • 修改JwtUtil中的parseToken方法

    由于需要从Jwt中获取用户id,因此需要为parseToken 方法增加返回值,如下

    public static Claims parseToken(String token){if (token==null){throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);}try{JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();return jwtParser.parseClaimsJws(token).getBody();}catch (ExpiredJwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);}catch (JwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);}
    }
    
  • 编写ThreadLocal工具类

    理论上我们可以在Controller方法中,使用@RequestHeader获取JWT,然后在进行解析,如下

    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info(@RequestHeader("access-token") String token) {Claims claims = JwtUtil.parseToken(token);Long userId = claims.get("userId", Long.class);SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);return Result.ok(userInfo);
    }
    

    上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。

    ThreadLocal概述

    ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,使每个线程都可以操作自己的变量,而不会互相干扰,其用法如下图所示。

    common模块中创建com.atguigu.lease.common.login.LoginUserHolder工具类

    public class LoginUserHolder {public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();public static void setLoginUser(LoginUser loginUser) {threadLocal.set(loginUser);}public static LoginUser getLoginUser() {return threadLocal.get();}public static void clear() {threadLocal.remove();}
    }
    

    同时在common模块中创建com.atguigu.lease.common.login.LoginUser

    @Data
    @AllArgsConstructor
    public class LoginUser {private Long userId;private String username;
    }
    
  • 修改AuthenticationInterceptor拦截器

    @Component
    public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("access-token");Claims claims = JwtUtil.parseToken(token);Long userId = claims.get("userId", Long.class);String username = claims.get("username", String.class);LoginUserHolder.setLoginUser(new LoginUser(userId, username));return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {LoginUserHolder.clear();}
    }
    
  • 编写Controller层逻辑

    LoginController中增加如下内容

    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info() {SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());return Result.ok(userInfo);
    }
    
  • 编写Service层逻辑

    LoginService中增加如下内容

    @Override
    public SystemUserInfoVo getLoginUserInfo(Long userId) {SystemUser systemUser = systemUserMapper.selectById(userId);SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();systemUserInfoVo.setName(systemUser.getName());systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());return systemUserInfoVo;
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/855949.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud Netflix和SpringCloud Alibaba核心组件

1.SpringCloud Netflix组件 1.1 Netflix Eureka-服务注册发现 Eureka 是一种用于服务发现 的组件&#xff0c;它是一个基于 REST 的服务&#xff0c;用于定位运行在 AWS 弹性计算云&#xff08;EC2&#xff09;中的中间层服务&#xff0c;以便它们可以相互通讯。 注册&#xf…

day14-226.翻转二叉树+101. 对称二叉树+104.二叉树的最大深度

一、226.翻转二叉树 题目链接&#xff1a;https://leetcode.cn/problems/invert-binary-tree/ 文章讲解&#xff1a;https://programmercarl.com/0226.%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE 视频讲解&#xff1…

C++ —— unordered_set、unordered_map的介绍及使用

目录 unordered系列关联式容器 unordered_set的介绍 unordered_set的使用 unordered_set的定义方式 unordered_set接口的使用 unordered_multiset unordered_map的介绍 unordered_map的使用 unordered_map的定义方式 unordered_map接口的使用 unordered_multimap …

智慧校园发展趋势:2024年及未来教育科技展望

展望2024年及未来的教育科技领域&#xff0c;智慧校园的发展正引领着一场教育模式的深刻变革&#xff0c;其核心在于更深层次地融合技术与教育实践。随着人工智能技术的不断成熟&#xff0c;个性化学习将不再停留于表面&#xff0c;而是深入到每个学生的个性化需求之中。通过精…

密钥管理简介

首先我们要知道什么是密钥管理&#xff1f; 密钥管理是一种涉及生成、存储、使用和更新密钥的过程。 密钥的种类 我们知道&#xff0c;对称密码主要包括分组密码和序列密码。但有时也可以将杂凑函数和消息认证码划分为这一类&#xff0c;将它们的密钥称为对称密钥&#xff1b;…

白酒:酒文化的地域特色与差异

中国的白酒文化&#xff0c;作为一种深深植根于人们生活中的文化现象&#xff0c;其发展历程深受地域特色的影响&#xff0c;从而形成了丰富多样的地域特色与差异。云仓酒庄的豪迈白酒&#xff0c;作为中国白酒的品牌&#xff0c;其背后所蕴含的地域特色与差异更是值得我们去探…

在 Ubuntu 18.04.4 LTS上安装 netmap

文章目录 步骤运行配置文件编译安装使用netmap 步骤 sudo su sudo apt-get update sudo apt install build-essential sudo apt-get install -y git sudo apt-get install -y linux-headers-$(uname -r)rootVM-20-6-ubuntu:/home/ubuntu/netmap/LINUX# git clone https://gith…

外观模式(大话设计模式)C/C++版本

外观模式 C #include <iostream> using namespace std;class stock1 { public:void Sell(){cout << "股票1卖出" << endl;}void Buy(){cout << "股票1买入" << endl;} };class stock2 { public:void Sell(){cout << …

el-cascader 支持多层级,多选(可自定义限制数量),保留最后一级

多功能的 el-cascader 序言&#xff1a;最近遇到一个需求关于级联的&#xff0c;有点东西&#xff0c;这里是要获取某个产品类型下的产品&#xff0c;会存在产品类型和产品在同一级的情况&#xff0c;但是产品类型不能勾选&#xff1b; 情况1&#xff08;二级菜单是产品&…

【mysql】排错和调优

通用的一些排错方法。 1、查看进程信息 mysql> show full processlist;mysql> show processlist; 2、information_schema information_schema这张数据表保存了MySQL服务器所有数据库的信息。如数据库名&#xff0c;数据库的表&#xff0c;表栏的数据类型与访问权限等。…

DDP算法之反向传播(Backward Pass)

DDP算法反向传播 在DDP(Differential Dynamic Programming)算法中,反向传播(Backward Pass)是关键步骤之一。这个步骤的主要目的是通过动态规划递归地计算每个时间步上的值函数和控制策略,以便在前向传播(Forward Pass)中使用。 反向传播的目标 反向传播的主要目标是…

AD层次原理图绘制

一、在原理图中添加端口 二、添加层次图 三、更新层次图 四、也可以先画层次图&#xff0c;再绘制原理图&#xff0c;这里就不做演示了

vue3+ts 使用vue3-ace-editor实现Json编辑器

1、效果图 输入代码&#xff0c;点击格式化就出现以上效果&#xff0c;再点击压缩&#xff0c;是以下效果2、安装 npm i vue3-ace-editor 3、使用 新建aceConfig.js文件 // ace配置&#xff0c;使用动态加载来避免第一次加载开销 import ace from ace-builds// 导入不同的主…

快速定制新人入职,可以自动回答知识库问题的机器人 | Chatopera

定制智能对话机器人&#xff0c;比如我是 HR&#xff0c;想要做一个聊天机器人&#xff0c;帮助新人入职&#xff0c;自动回答知识库的问题。 注册 Chatopera 云服务 首先&#xff0c;您需要一个 Chatopera 云服务账户&#xff0c;然后根据下面四个步骤实现。 服务注册地址&…

UnityAPI学习之碰撞检测与触发检测

碰撞检测 发生碰撞检测的前提&#xff1a; 1. 碰撞的物体需要有Rigidbody组件和boxcllidder组件 2. 被碰撞的物体需要有boxcollider组件 示例1&#xff1a;被碰撞的物体拥有Rigidbody组件 两个物体会因为都具有刚体的组件而发生力的作用&#xff0c;如下图所示&#xff0c…

创建一个electron桌面备忘录

Sound Of Silence 1.创建electron项目命令&#xff1a; npm create quick-start/electron my-new-project 2选择&#xff1a;√ Select a framework: vue √ Add TypeScript? ... No √ Add Electron updater plugin? ... Yes √ Enable Electron download mirror proxy? .…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 身高差值排序(100分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f…

22种常用设计模式示例代码

文章目录 创建型模式结构型模式行为模式 仓库地址https://github.com/Xiamu-ssr/DesignPatternsPractice 参考教程 refactoringguru设计模式-目录 创建型模式 软件包复杂度流行度工厂方法factorymethod❄️⭐️⭐️⭐️抽象工厂abstractfactory❄️❄️⭐️⭐️⭐️生成器bui…

如何配置node.js环境

文章目录 step1. 下载node.js安装包step2. 创建node_global, node_cache文件夹step3.配置node环境变量step3. cmd窗口检查安装的node和npm版本号step4. 设置缓存路径\全局安装路径\下载镜像step5. 测试配置的nodejs环境 step1. 下载node.js安装包 下载地址&#xff1a;node.js…