2.SpringBoot3 + sa-token实现基础登录

1,069 阅读10分钟

相关版本

springBoot => 3.3.0

sa-token => 1.38.0

knife4j => 4.5.0

mybatis-plus => 3.5.6

lombok => 1.18.32

mapstruct => 1.6.0.Beta2

mysql => 8.0.34

Mysql数据库设计

数据库设计在我上一篇博客:juejin.cn/post/737498…

SpringBoot程序

导入依赖

 <dependencies>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
         <exclusions>
             <!-- 排除spring-boot-starter-web内自带的日志插件 -->
             <exclusion>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-logging</artifactId>
             </exclusion>
         </exclusions>
     </dependency>
 ​
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
     </dependency>
 ​
     <!-- 使用 SpringBoot Validation 注解对参数进行校验 -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-validation</artifactId>
         <!--<version>3.3.0</version>-->
     </dependency>
 ​
     <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
     <dependency>
         <groupId>cn.dev33</groupId>
         <artifactId>sa-token-spring-boot3-starter</artifactId>
         <version>1.38.0</version>
     </dependency>
 ​
     <!-- Knife4j是基于springboot构建的一个文档生成工具,它可以让开发者为我们的应用生成API文档
                 =>
                 访问地址:项目根地址/doc.html => https://doc.xiaominfo.com/docs/quick-start -->
     <dependency>
         <groupId>com.github.xiaoymin</groupId>
         <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
         <version>4.5.0</version>
     </dependency>
     <!-- mybatis-plus -->
     <dependency>
         <groupId>org.mybatis</groupId>
         <artifactId>mybatis-spring</artifactId>
         <version>3.0.3</version>
     </dependency>
     <dependency>
         <groupId>com.baomidou</groupId>
         <artifactId>mybatis-plus-boot-starter</artifactId>
         <version>3.5.6</version>
         <exclusions>
             <exclusion>
                 <groupId>org.mybatis</groupId>
                 <artifactId>mybatis-spring</artifactId>
             </exclusion>
         </exclusions>
     </dependency>
     <!--  lombok  -->
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.18.32</version>
         <scope>provided</scope>
     </dependency>
     <!-- mysql连接 -->
     <dependency>
         <groupId>com.mysql</groupId>
         <artifactId>mysql-connector-j</artifactId>
         <version>8.4.0</version>
     </dependency>
     <!-- devtools 热启动 -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-devtools</artifactId>
         <scope>runtime</scope>
         <optional>true</optional>
     </dependency>
     <!--  fastjson  -->
     <dependency>
         <groupId>com.alibaba.fastjson2</groupId>
         <artifactId>fastjson2</artifactId>
         <version>2.0.50</version>
     </dependency>
     <!--  log4j2  -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-log4j2</artifactId>
         <version>3.3.0</version>
     </dependency>
     <!--mapstruct核心-->
     <dependency>
         <groupId>org.mapstruct</groupId>
         <artifactId>mapstruct</artifactId>
         <version>1.6.0.Beta2</version>
     </dependency>
     <!--mapstruct编译-->
     <dependency>
         <groupId>org.mapstruct</groupId>
         <artifactId>mapstruct-processor</artifactId>
         <version>1.6.0.Beta2</version>
     </dependency>
 ​
 </dependencies>

Knife4j配置文件

 package com.ayo.config;
 ​
 import io.swagger.v3.oas.models.ExternalDocumentation;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.info.Contact;
 import io.swagger.v3.oas.models.info.Info;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 @Configuration
 public class Knife4jConfig {
     @Bean
     public OpenAPI customOpenAPI() {
 ​
         //创建一个 OpenAPI 对象,用于表示整个 API 的文档信息
         return new OpenAPI()
                 // 接口文档标题
                 .info(new Info().title("API接口文档")
                         // 接口文档简介
                         .description("我的第一个Knife4j")
                         // 接口文档版本
                         .version("0.0.1-SNAPSHOT")
                         // 开发者的联系方式,包括姓名和电子邮件地址
                         .contact(new Contact().name("Ayo").email("2192475085@qq.com")));
     }
 ​
 }
 ​

配置yaml文件

 server:
   # 端口
   port: 8081
 ############## mysql配置 ##############
 spring:
   datasource:
     # 链接数据库
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
     password: 123456
     url: jdbc:mysql://localhost:3306/sa_token?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
   security:
     user:
       name: admin
       password: 123456
 
 ############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
 sa-token:
   # token 名称(同时也是 cookie 名称)
   token-name: satoken
   # token 有效期(单位:秒) 默认30天,-1 代表永久有效
   timeout: 2592000
   # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
   active-timeout: -1
   # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
   is-concurrent: true
   # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
   is-share: true
   # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
   token-style: uuid
   # 是否输出操作日志
   is-log: false
 
 ############## mybatisPlus配置 ##############
 # mybatisPlus配置
 mybatis-plus:
   # mapper映射地址
   mapper-locations: classpath:mapper/*.xml
   # 实体类扫描包路径
   type-aliases-package: com.ayo.entity
 #  configuration:
 #    # sql打印
 #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 #    # 开启驼峰命名
 #    map-underscore-to-camel-case: true
 #  global-config:
 #    db-config:
 #      # 数据库表前缀
 #      table-prefix: m_
 
 
 ############## 自定义knife4j配置 ##############
 # springdoc-openapi项目访问访问地址: http://127.0.0.1:8080/doc.html
 springdoc:
   swagger-ui:
     path: /swagger-ui.html
     # path: 配置swagger-ui.html/UI界面的访问路径,默认为/swagger-ui.html
     tags-sorter: alpha
     # tags-sorter: 接口文档中的tags排序规则,默认为alpha,可选值为alpha(按字母顺序排序)或as-is(按照在代码中定义的顺序排序)
     operations-sorter: alpha
 
   api-docs:
     path: /v3/api-docs
     # path: 配置api-docs的访问路径,默认为/v3/api-docs
 
   group-configs:
     # group-configs: 配置分组信息
     - group: 'default'
       # group: 分组名称
       paths-to-match: '/**'
       # paths-to-match: 配置要匹配的路径,默认为/**
       packages-to-scan: com.ayo.controller
       # packages-to-scan: 配置要扫描的包的路径,直接配置为Controller类所在的包名即可
 
 # knife4j项目访问访问地址:http://127.0.0.1:8080/doc.html#/home
 knife4j:
   enable: true
   # 设置为true以启用Knife4j增强功能,这将再应用程序中启用Knife4j UI
   setting:
     # language: 设置Knife4j UI的语言,默认为zh_cn,可选值为zh_cn或en
     language: zh_cn
   #开启生产环境屏蔽
   production: false
   #是否启用登录认证
   basic:
     enable: true
     username: root # 自己设置一个
     password: 123456 # 自己设置一个

注意knife4j配置下的usernamepassword;后续使用knife4j接口调试地址时访问doc.html地址需要输入这里的用户名和密码才运行访问


状态码枚举类

 package com.ayo.enums;
 ​
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 ​
 /**
  * 状态码枚举
  *
  * @author ayo
  */
 ​
 public enum StatusCodeEnum {
     /**
      * 操作成功
      */
     SUCCESS(200, "操作成功"),
 ​
     /**
      * 参数错误
      */
     VALID_ERROR(400, "参数错误"),
 ​
     /**
      * 未登录
      */
     UNAUTHORIZED(402, "未登录"),
 ​
     /**
      * 系统异常
      */
     SYSTEM_ERROR(-1, "系统异常"),
 ​
     /**
      * 操作失败
      */
     FAIL(500, "操作失败");
 ​
     /**
      * 状态码
      */
     private final Integer code;
 ​
 ​
     /**
      * 返回信息
      */
     private final String msg;
 ​
     public Integer getCode() {
         return code;
     }
 ​
     public String getMsg() {
         return msg;
     }
 ​
     StatusCodeEnum(Integer code, String msg) {
         this.code = code;
         this.msg = msg;
     }
 ​
 }

sa-token全局过滤器

官网地址:sa-token.cc/doc.html#/u…

 package com.ayo.saToken;
 ​
 ​
 import cn.dev33.satoken.context.SaHolder;
 import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.filter.SaServletFilter;
 import cn.dev33.satoken.router.SaRouter;
 import cn.dev33.satoken.stp.StpUtil;
 import cn.dev33.satoken.util.SaResult;
 import com.alibaba.fastjson2.JSON;
 import com.ayo.model.vo.Result;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 import static com.ayo.enums.StatusCodeEnum.UNAUTHORIZED;
 ​
 ​
 /**
  * [Sa-Token 权限认证] 配置类 => https://sa-token.cc/doc.html#/up/global-filter
  */
 ​
 ​
 @Configuration
 public class SaTokenConfigure {
 ​
     // 放行路径
     private final String[] EXCLUDE_PATH_PATTERNS = {
             "/swagger-resources",
             "/webjars/**",
             "/v2/api-docs",
             "/doc.html",
             "/favicon.ico",
             "/login",
             "/oauth/*",
     };
 ​
     private final long timeout = 600;
 ​
 ​
     /**
      * 注册 [Sa-Token全局过滤器]
      */
     @Bean
     public SaServletFilter getSaServletFilter() {
         return new SaServletFilter()
                 // 拦截路径
                 .addInclude("/**")
                 // 放行路由
                 .addExclude(EXCLUDE_PATH_PATTERNS)
                 // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
                 .setBeforeAuth(r -> {
                     // ---------- 设置一些安全响应头 ----------
                     SaHolder.getResponse()
                             // 服务器名称
                             .setServer("sa-server")
                             // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
                             .setHeader("X-Frame-Options", "SAMEORIGIN")
                             // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
                             .setHeader("X-XSS-Protection", "1; mode=block")
                             // 禁用浏览器内容嗅探
                             .setHeader("X-Content-Type-Options", "nosniff")
                             /* ---------------------------------------------------------- */
                             // 允许指定域访问跨域资源
                             .setHeader("Access-Control-Allow-Origin", "*")
                             // 允许所有请求方式
                             .setHeader("Access-Control-Allow-Methods", "*")
                             // 有效时间
                             .setHeader("Access-Control-Max-Age", "3600")
                             // 允许的header参数
                             .setHeader("Access-Control-Allow-Headers", "*");
                     ;
                 })
                 // 认证函数: 每次请求执行
                 .setAuth(obj -> {
                     System.out.println("请求拦截 => ");
                     // 检查是否登录
                     SaRouter.match("/admin/**").check(r -> StpUtil.checkLogin());
                     // 刷新token有效期
                     if (StpUtil.getTokenTimeout() < timeout) {
                         StpUtil.renewTimeout(1800);
                     }
                     // 输出 API 请求日志,方便调试代码
 //                    SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
                 })
                 //  异常处理函数:每次认证函数发生异常时执行此函数
                 .setError(e -> {
                     // 设置响应头
                     SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
                     if (e instanceof NotLoginException) {
                         return JSON.toJSONString(Result.fail(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMsg()));
                     }
                     // TODO 服务器后端在这里无法捕获异常,仅仅将异常信息传给了前端
                     e.printStackTrace();
                     return SaResult.error(e.getMessage());
                 });
     }
 ​
 }

统一接口返回数据格式(全局vo)

 package com.ayo.model.vo;
 ​
 ​
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 ​
 import static com.ayo.enums.StatusCodeEnum.FAIL;
 import static com.ayo.enums.StatusCodeEnum.SUCCESS;
 ​
 /**
  * 结果返回类
  *
  * @author ayo
  */
 @Data
 @Schema(description = "结果返回类")
 public class Result<T> {
 ​
     /**
      * 返回状态
      */
     @Schema(description = "返回状态")
     private Boolean flag;
 ​
     /**
      * 状态码
      */
     @Schema(description = "状态码")
     private Integer code;
 ​
     /**
      * 返回信息
      */
     @Schema(description = "返回信息")
     private String msg;
 ​
     /**
      * 返回数据
      */
     @Schema(description = "返回数据")
     private T data;
 ​
 ​
     /**
      * 无参静态方法,用于创建一个表示成功结果的 Result 对象。
      * 返回一个 Result 对象,其中 flag 被设置为 true,code 被设置为成功状态码,msg 被设置为成功信息,而 data 为 null
      * */
     public static <T> Result<T> success() {
         return buildResult(true, null, SUCCESS.getCode(), SUCCESS.getMsg());
     }
 ​
     /**
      * 接受泛型参数 data,用于创建一个包含成功数据的 Result 对象。
      * 返回一个 Result 对象,其中 flag 被设置为 true,code 被设置为成功状态码,msg 被设置为成功信息,而 data 被设置为传入的数据。
      * */
     public static <T> Result<T> success(T data) {
         return buildResult(true, data, SUCCESS.getCode(), SUCCESS.getMsg());
     }
 ​
     /**
      * 接受一个字符串参数 message,用于创建一个表示失败结果的 Result 对象。
      * 返回一个 Result 对象,其中 flag 被设置为 false,code 被设置为失败状态码,msg 被设置为传入的消息,而 data 为 null。
      * */
     public static <T> Result<T> fail(String message) {
         return buildResult(false, null, FAIL.getCode(), message);
     }
 ​
     /**
      * 接受一个整数参数 code 和一个字符串参数 message,用于创建一个表示失败结果的 Result 对象。
      * 返回一个 Result 对象,其中 flag 被设置为 false,code 被设置为传入的状态码,msg 被设置为传入的消息,而 data 为 null。
      * */
     public static <T> Result<T> fail(Integer code, String message) {
         return buildResult(false, null, code, message);
     }
 ​
     /**
      * 私有静态方法,用于构建一个 Result 对象。
      * 接受 flag、data、code 和 message 四个参数,并返回一个根据这些参数构建的 Result 对象。
      * */
     private static <T> Result<T> buildResult(Boolean flag, T data, Integer code, String message) {
         Result<T> r = new Result<>();
         r.setFlag(flag);
         r.setData(data);
         r.setCode(code);
         r.setMsg(message);
         return r;
     }
 }
 ​

封装 全局异常处理

 package com.ayo.handler;
 ​
 import cn.dev33.satoken.exception.DisableServiceException;
 import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.exception.NotPermissionException;
 import cn.dev33.satoken.exception.NotRoleException;
 import com.ayo.model.vo.Result;
 import jakarta.validation.ConstraintViolationException;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 ​
 import java.util.Objects;
 ​
 import static com.ayo.enums.StatusCodeEnum.*;
 ​
 /**
  * 全局异常处理
  *
  * @author ayo
  */
 ​
 ​
 @RestControllerAdvice
 public class GlobalExceptionHandler {
 ​
     /**
      * 处理SpringBoot Validation 参数验证抛出的异常
      * */
     //处理MethodArgumentNotValidException,该异常会在请求参数验证失败时抛出
     @ExceptionHandler(MethodArgumentNotValidException.class)
     public Result<?> handleValidationException(MethodArgumentNotValidException e) {
         return Result.fail(HttpStatus.BAD_REQUEST.value(),e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
     }
     //处理ConstraintViolationException,该异常会在方法级别的参数验证失败时抛出
     @ExceptionHandler(ConstraintViolationException.class)
     public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
         return Result.fail(HttpStatus.BAD_REQUEST.value(),e.getConstraintViolations().iterator().next().getMessage());
     }
 ​
     /**
      * 处理Assert异常
      */
     @ExceptionHandler(AssertionError.class)
     public Result<?> handleAssertionError(AssertionError e) {
         return Result.fail(e.getMessage());
     }
 ​
     /**
      * 处理权限不足
      */
     @ExceptionHandler(value = NotPermissionException.class)
     public Result<?> handleNotPermissionException() {
         return Result.fail("权限不足");
     }
 ​
     /**
      * 处理账号封禁
      */
     @ExceptionHandler(value = DisableServiceException.class)
     public Result<?> handleDisableServiceExceptionException() {
         return Result.fail("此账号已被禁止访问服务");
     }
 ​
     /**
      * 处理无此角色异常
      */
     @ExceptionHandler(value = NotRoleException.class)
     public Result<?> handleNotRoleException() {
         return Result.fail("权限不足");
     }
 ​
     /**
      * 处理SaToken异常
      */
     @ExceptionHandler(value = NotLoginException.class)
     public Result<?> handlerNotLoginException(NotLoginException nle) {
         // 判断场景值,定制化异常信息
         String message;
         if (nle.getType().equals(NotLoginException.NOT_TOKEN)) {
             message = "未提供token";
         } else if (nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
             message = "token无效";
         } else if (nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
             message = "token已过期";
         } else {
             message = "当前会话未登录";
         }
         // 返回给前端
         return Result.fail(UNAUTHORIZED.getCode(), message);
     }
 ​
     /**
      * 处理系统异常
      */
     @ExceptionHandler(value = Exception.class)
     public Result<?> handleSystemException() {
         return Result.fail(SYSTEM_ERROR.getCode(), SYSTEM_ERROR.getMsg());
     }
 ​
 }

定义user的modul对象(vo)

登录请求数据对象

 package com.ayo.model.vo.request;
 ​
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 ​
 /**
  * 登录请求传入的用户数据
  */
 ​
 @Data
 @Schema(description = "用户登录对象")
 public class LoginReq {
 ​
     @NotBlank(message = "用户名不能为空")
     //@Schema(description = "用户名",required = true) // required = true弃用了
     @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
     private String username;
 ​
     @NotBlank(message = "密码不能为空")
     @Size(min = 6, message = "密码不能少于6位")
     @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
     private String password;
 }

登录成功响应个前端的数据对象

 package com.ayo.model.vo.response;
 ​
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 ​
 import java.sql.Timestamp;
 ​
 /**
  * 登录成功后相应的数据对象
  * */
 ​
 @Data
 @Schema(description = "用户登录成功后的数据对象")
 public class LoginRes {
 ​
     @Schema(description = "用户id")
     private String id;
 ​
     @Schema(description = "用户名")
     private String username;
 ​
     @Schema(description = "token")
     private String token;
 ​
     @Schema(description = "用户创建时间")
     @TableField("`create`")
     private Timestamp create;
 ​
     @Schema(description = "用户更新时间")
     @TableField("`update`")
     private Timestamp update;
 ​
 }

entity层

 package com.ayo.entity;
 ​
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.v3.oas.annotations.Hidden;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 ​
 import java.sql.Timestamp;
 ​
 /**
  * 用户表
  * */
 ​
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @Schema(description = "用户对象")
 @TableName("sa_user")
 public class User {
 ​
     @Schema(description = "用户id")
     @TableId("id")
     private String id;
 ​
     @Schema(description = "用户名")
     @TableField("username")
     private String username;
 ​
 ​
     @Hidden
     @Schema(description = "密码")
     @TableField("password")
     private String password;
 ​
     @Schema(description = "用户是否启用")
     @TableField("enable")
     private boolean enable;
 ​
     @Schema(description = "用户是否删除")
     @TableField("is_delete")
     private boolean isDelete;
 ​
     @Schema(description = "用户创建时间")
     @TableField("`create`")
     private Timestamp create;
 ​
     @Schema(description = "用户更新时间")
     @TableField("`update`")
     private Timestamp update;
 ​
 }

用户实体类 各种数据类型间的转换(vo <--> entity)

由于登录涉及到 vo实体类的数据之间的转换,这里使用了mapstruct

即登录的时候使用 LoginReq

登录成功后使用LoginRes

具体使用教程参考:blog.csdn.net/zhouzhiweng…

package com.ayo.mapstruct;

import com.ayo.entity.User;
import com.ayo.model.vo.response.LoginRes;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

/**
 * mapstruct 工具类定义步骤:
 * 1、添加MapStruct jar包依赖
 * 2、新增接口或抽象类,并且使用org.mapstruct.Mapper注解标签修饰。
 * 3、添加自定义转换方法
 */

/**
 * 用户实体类 各种数据转换
 */

//@Mapper //org.mapstruct.Mapper
@Mapper(componentModel = "spring") //org.mapstruct.Mapper
public interface UserConvert {

    UserConvert INSTANCE = Mappers.getMapper( UserConvert.class );

    /* 实体类 转 登录响应结果数据对象 */
    LoginRes toLoginRes(User user);

    /*  登录响应结果数据对象 转 实体类  */
    User toUserEntity(LoginRes loginRes);

}

@Mapperorg.mapstruct.Mapper类下的,配置componentModel = "spring",让spring对其进行托管


Dao层

继承 Mybatis-plus 类

package com.ayo.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;


@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}

service层

接口
package com.ayo.service;

import com.ayo.entity.User;
import com.ayo.model.vo.request.LoginReq;
import com.ayo.model.vo.response.LoginRes;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {

    /* 用户登录请求 */
    LoginRes login(LoginReq login);

}
接口实现类
 package com.ayo.service.Impl;
 ​
 import cn.dev33.satoken.secure.BCrypt;
 import cn.dev33.satoken.stp.StpUtil;
 import com.ayo.dao.UserMapper;
 import com.ayo.entity.User;
 import com.ayo.mapstruct.UserConvert;
 import com.ayo.model.vo.request.LoginReq;
 import com.ayo.model.vo.response.LoginRes;
 import com.ayo.service.UserService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
 ​
 @Service
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
 ​
     @Autowired
     private UserMapper userMapper;
 ​
     @Autowired
     private UserConvert userConvert;
 ​
     /* 用户登录请求 */
     @Override
     public LoginRes login(LoginReq login) {
 ​
         User user = userMapper.selectOne(
                 new LambdaQueryWrapper<User>()
                         .select(User::getId,User::getUsername,User::getPassword,User::getCreate,User::getUpdate)
                         .eq(User::getUsername, login.getUsername())
                         //.eq(User::getPassword, BCrypt.checkpw(login.getPassword(User::getPassword)))
         );
 ​
         /**
          * assert(条件语句),如果条件语句为真,继续往下执行,如果为假就会报错
          * */
         Assert.notNull(user, "用户名不存在");
         Assert.isTrue(BCrypt.checkpw(login.getPassword(), user.getPassword()),"密码错误");
 ​
         //if(!BCrypt.checkpw(login.getPassword(),user.getPassword())){
         //    return "密码错误";
         //}
 ​
         // 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
         StpUtil.checkDisable(user.getId());
         // 通过校验后,再进行登录
         StpUtil.login(user.getId());
         
         // entity数据转为vo响应的数据
         LoginRes loginRes = userConvert.toLoginRes(user);
         loginRes.setToken(StpUtil.getTokenValue());
 ​
         return loginRes;
     }
 }
 ​

controller层

 package com.ayo.controller;
 ​
 import com.ayo.model.vo.Result;
 import com.ayo.model.vo.request.LoginReq;
 import com.ayo.service.UserService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 ​
 @Tag(name = "用户登录", description = "用户登录相关")
 @RestController
 public class LoginController {
 ​
     @Autowired
     UserService userService;
 ​
     @Operation(summary = "用户登录")
     @PostMapping("/login")
     public Result login(@Validated @RequestBody LoginReq login) {
         return Result.success(userService.login(login));
     }
 }
 ​

测试

测试地址:http://127.0.0.1:8081/doc.html

1.登录接口调试地址

image.png

2.登录验证

image.png

源代码地址:gitee.com/CnAyo/code-…