Back

Springboot脚手架开发

springboot3.4 开发脚手架入门-整合各种功能

SpringBoot3x 脚手架开发 - 整合各种功能

总结一下自己的springboot 开发模板。依赖导入和整合各种工具。(个人学习使用)

springboot 3.4.0

idea 起步依赖的选择

  1. Developer Tools
    1. Spring Boot DevTools
    2. Lombok
  2. Web
    1. Spring Web
  3. SQL
    1. MySQL Driver
    2. MyBatis FrameWork

在idea 中创建其实也是依赖官网提供的去生成的。spring initializr

常规项目结构

参考酒老师的template:https://gitee.com/mi9688-wine/springboot-template

img
img

controller: 负责接收前端的请求、调用适当的业务逻辑、将处理的结构返回给用户类。通常控制层是不写任何业务逻辑的,它的作用主要把业务功能暴漏为接口,再者进行参数校验.

Service: 来访各种业务功能规范接口和对应的实现类的。

mapper: 该层存放数据访问接口类,通常只需要定义出接口,具体的操作数据库的逻辑是借助ORM(对象关系映射)框架—mybatis/mybatis-plus/jpa等来快捷编写或者直接生成的。

这也就是常规的三层结构。


**Common:**此目录用于存放全局会用到的一些静态常量类、枚举类、业务异常类、工具类、自定义注解、切面类、DTO、VO、配置类等都可以放在该目录下

img
img

entity: Entity类通常与数据库表中的记录(Row)对应,它们之间存在一一对应的关系。

test: 主要用来放mapper层、service层的测试用例类.

统一数据返回格式

放在:result.handle

作为一种规范,其主要目的是:提高效率(复用)、提供统一的API响应格式,简化前后端交互。

常见格式:(具体格式还是看团队,学习就用这种格式)

1
2
3
4
5
{
	"code":1,
	"message": "success",
	"data": "data"
}

定义返回结果 枚举类(这里定义了基础的 0、1 和一些特殊业务错误的友好信息,后面需要进行配置)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import lombok.Getter;
 
/**
 * @author mijiupro(酒师傅)
 */
@Getter
public enum ResultEnum {
 
    /* 成功状态码 */
    SUCCESS(1, "操作成功!"),
    /* 错误状态码 */
    FAIL(0, "操作失败!"),
 
    /* 参数错误:10001-19999 */
    PARAM_IS_INVALID(10001, "参数无效"),
    PARAM_IS_BLANK(10002, "参数为空"),
    PARAM_TYPE_BIND_ERROR(10003, "参数格式错误"),
    PARAM_NOT_COMPLETE(10004, "参数缺失"),
 
    /* 用户错误:20001-29999*/
    USER_NOT_LOGGED_IN(20001, "用户未登录,请先登录"),
    USER_LOGIN_ERROR(20002, "账号不存在或密码错误"),
    USER_ACCOUNT_FORBIDDEN(20003, "账号已被禁用"),
    USER_NOT_EXIST(20004, "用户不存在"),
    USER_HAS_EXISTED(20005, "用户已存在"),
 
    /* 系统错误:40001-49999 */
    FILE_MAX_SIZE_OVERFLOW(40003, "上传尺寸过大"),
    FILE_ACCEPT_NOT_SUPPORT(40004, "上传文件格式不支持"),
 
    /* 数据错误:50001-599999 */
    RESULT_DATA_NONE(50001, "数据未找到"),
    DATA_IS_WRONG(50002, "数据有误"),
    DATA_ALREADY_EXISTED(50003, "数据已存在"),
    AUTH_CODE_ERROR(50004, "验证码错误"),
 
 
    /* 权限错误:70001-79999 */
    PERMISSION_UNAUTHENTICATED(70001, "此操作需要登陆系统!"),
    PERMISSION_UNAUTHORIZED(70002, "权限不足,无权操作!"),
    PERMISSION_EXPIRE(70003, "登录状态过期!"),
    PERMISSION_TOKEN_EXPIRED(70004, "token已过期"),
    PERMISSION_LIMIT(70005, "访问次数受限制"),
    PERMISSION_TOKEN_INVALID(70006, "无效token"),
    PERMISSION_SIGNATURE_ERROR(70007, "签名失败");
 
    // 状态码
    int code;
    // 提示信息
    String message;
 
    ResultEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }
 
    public int code() {
        return code;
    }
 
    public String message() {
        return message;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
}

定义返回结果的封装类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.mijiu.commom.enumerate.ResultEnum;
import lombok.Data;
/**
 * @author mijiupro
 */
@Getter
@Setter
public class Result<T> {
    // 操作代码
    Integer code;
    // 提示信息
    String message;
    // 结果数据
    T data;
    // 提供不同的构造方法
    public Result(ResultEnum resultCode) {
        this.code = resultCode.code();
        this.message = resultCode.message();
    }
 
    public Result(ResultEnum resultCode, T data) {
        this.code = resultCode.code();
        this.message = resultCode.message();
        this.data = data;
    }
    public Result(String message) {
        this.message = message;
    }
    // 提供一些静态方法,以便能快速的返回对应的对象。
    //成功返回封装-无数据
    public static Result<String> success() {
        return new Result<String>(ResultEnum.SUCCESS);
    }
    //成功返回封装-带数据
    public static <T> Result<T> success(T data) {
        return new Result<T>(ResultEnum.SUCCESS, data);
    }
    //失败返回封装-使用默认提示信息
    public static Result<String> error() {
        return new Result<String>(ResultEnum.FAIL);
    }
    //失败返回封装-使用返回结果枚举提示信息
    public static Result<String> error(ResultEnum resultCode) {
        return new Result<String>(resultCode);
    }
    //失败返回封装-使用自定义提示信息
    public static Result<String> error(String message) {
        return new Result<String>(message);
    }
}

我们的接口通常有很多,如果对每个接口都封装属于重复劳动,可以利用AOP技术拦截控制类的返回结果进行封装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import cn.hutool.json.JSONUtil;
import com.mijiu.commom.result.Result;
import io.micrometer.common.lang.NonNullApi;
import io.micrometer.common.lang.Nullable;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
 
/**
 * 统一结果封装增强器
 * @author mijiupro
 */
@RestControllerAdvice(basePackages = "com.mijiu.controller")//指定要增强的包
@NonNullApi
public class ResultAdvice implements ResponseBodyAdvice<Object> {
 
    /**
     * 判断是否支持对返回类型的处理
     *
     * @param returnType    方法参数的类型
     * @param converterType 转换器的类型
     * @return 是否支持处理
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
 
    /**
     * 在写入响应体之前对返回结果进行处理和封装
     *
     * @param body                  返回结果对象
     * @param returnType            方法参数的类型
     * @param selectedContentType   响应内容的类型
     * @param selectedConverterType 转换器的类型
     * @param request               HTTP 请求对象
     * @param response              HTTP 响应对象
     * @return 处理后的返回结果
     */
    @Override
    public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        //当返回结果为字符串类型需要单独处理
        if (body instanceof String) {
            // 如果返回结果是字符串类型,将其封装为成功的结果对象,并转换为 JSON 字符串
            return JSONUtil.toJsonStr(Result.success(body));
        }
 
        // 将返回结果封装为成功的结果对象
        return Result.success(body);
    }
}

这个如果也会对knife4j的结果进行封装,导致其knife4j文档请求异常。

自定义异常处理器

存放到exception.handle目录中。

自定义异常处理器以及全局处理器。用以正确捕获异常,并返回对应的调试信息。以应对不可控和一些可以预知的异常。

通常将异常进行处理,封装一下对应错误信息返回友好信息(也就是前端一眼看得懂)。避免把异常直接给前端、用户。

步骤一:自定义异常类

  1. 通用业务:仅仅需要返回友好的错误提示信息。不需要其他操作。

定义一个通用业务异常类,然后在业务代码中手动抛出来触发对应的返回处理。(后面需要进行配置)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/**
 * @author rose
 * 通用业务异常类
 */
@Data
public class GeneralBusinessException extends RuntimeException{
    private int code=0;
    private String message;
 
    public GeneralBusinessException(String message) {
        this.message = message;
    }
 
}
  1. 特殊处理业务异常:需要进行额外的逻辑处理,比如密码错误次数上限,以避免爆破攻击等等。

定义一个密码错误处理类去捕获异常,(而对应的设置错误上限逻辑就需要在全局异常处理器中实现)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * 密码错误异常
 * @author rose
 */
@Data
public class PasswordErrorException extends RuntimeException {
    // 利用枚举类来维护错误信息(也就是上面统一响应结果的枚举类)
    private final ResultEnum resultEnum;
    
    public PasswordErrorException(ResultEnum resultEnum) {
        this.resultEnum = resultEnum;
    }
}

步骤二:创建全局异常处理器

定义的各种业务异常对应处理逻辑代码通常在全局异常处理器来写。

需要创建一个全局异常处理器类,用于捕获和处理所有的异常。通常使用 @RestControllerAdvice注解将该类标记为全局异常处理器,并使用**@ExceptionHandler** 注解定义具体的异常处理方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * @author mijiupro
 */
@RestControllerAdvice(basePackages = "com.mijiu.controller")
@ResponseBody
@Slf4j
public class GlobalExceptionHandler{
    
    // 特殊业务:密码错误异常
    @ExceptionHandler(PasswordErrorException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> handlePasswordErrorException(PasswordErrorException ex) {
        //如通过redis进行密码错误计数器累加操作
        //。。。。。。。。。。。。。
        
        //日志记录等。。。。。。。。。
 
        return Result.error(ex.getResultEnum());
    }
    //通用业务异常处理
    @ExceptionHandler(GeneralBusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> generalBusinessExceptionHandler(GeneralBusinessException ex) {
        return Result.error(ex.getMessage());
    }
    /**
     * 通用异常处理(用于处理不可预知的异常)
     */
    @ExceptionHandler(Exception.class) // 捕获所有异常
    public Result<String> exceptionHandler(Exception ex) {
        log.error(ex.getMessage());
        return Result.error(ResultEnum.FAIL);
    }
}

步骤三:手动抛出异常

对于可预见的异常:密码错误,以及通用业务异常等等都要手动抛出,就会响应对应的友好错误信息。

对不可预见的异常,则会被通用异常处理捕获,进行统一返回”操作失败“的信息。

常见依赖介绍

起步依赖就不再赘述了,介绍一下 常见工具包 hutoll-all

hutoll

Hutool 是一个 Java 工具包,提供了丰富的功能来简化 Java 开发中的常见任务。hutool-all 这个模块包含了 Hutool 提供的所有功能,主要包括但不限于以下几个方面:

字符串处理:包括字符串的切割、拼接、替换、格式化等操作。 日期时间处理:提供了日期时间的格式化、解析、计算、时间间隔等功能。 加密解密:支持常见的加密算法,包括 MD5、SHA、AES、RSA 等。 文件操作:提供了文件的读写、复制、移动、压缩、解压等功能。 HTTP 客户端:支持 HTTP 请求的发送和接收,包括 GET、POST 等方法。 邮件发送:支持邮件的发送和接收,包括 SMTP 协议的使用。 图片处理:支持图片的缩放、裁剪、水印、压缩等操作。 Excel 操作:支持 Excel 文件的读写、导入导出、样式设置等功能。 PDF 操作:支持 PDF 文件的读写、合并、拆分等操作。 JSON 解析:支持 JSON 字符串的解析和生成。 常用工具类:提供了各种常用的工具类,包括数组操作、集合操作、反射工具等。

1
2
3
4
5
 <dependency>
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.8.25</version>
 </dependency>

面向切面的编程AOP

1
2
3
4
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

整合各种功能使用

  1. mybatis-plus(1)
  2. 缓存中间件redis(2)
  3. swagger 接口测试(3)
  4. 参数校验(4)controller 层 dto
  5. 认证鉴权 -jwt(5)

Mybatis-plus

引入依赖:(如果你遇到问题,尝试手动引入一个更高版本的mybatis依赖。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

编写配置文件:连接信息,数据源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接数据库的url
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8 
    username: root
    password: 111111

#mp 基础配置 
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    #打印sql日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 
  global-config:
    db-config:
      #id自增类型配置
      id-type: ASSIGN_ID
      #逻辑删除字段配置
      logic-delete-field: deleted
      logic-not-delete-value: 0
      logic-delete-value: 1
    #控制台mybatis-plus标记
    banner: true

有个比较重要的配置:开启驼峰命名自动转化为下划线。(Mybatis-plus是自动启动的。)

tips: 这里有个小坑就是如果数据库不是下划线格式的,就会报错。

Mybatis-plus 是mybatis 的增强,不需要手动写接口,其提供的就能满足简单业务逻辑的开发。

内置了通用Mapper,通用Service,且自动根据实体类生成了很多个基本的sql语句

1
2
3
@Mapper//表明这是一个Mapper,也可以在启动类上加上包扫描,添加要操作的实体类(也就对应着数据库)
public interface UserMapper extends BaseMapper<User> {
}

redis

依赖:

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置连接信息以及连接池参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
spring:
  data:
    redis: # Redis连接配置
      host: localhost  # Redis主机地址
      port: 6379  # Redis端口号
      password: 123456  # 访问Redis所需密码,配置密码才能进行数据操作
      database: 0  # 使用的数据库编号
      lettuce: #Lettuce客户端配置
        pool: # 连接池配置
          max-active: 8  # 最大活跃连接数
          max-wait: -1  # 最大等待时间(-1表示无限等待)
          max-idle: 8  # 最大空闲连接数
          min-idle: 0  # 最小空闲连接数

选择客户端:Lettuce(线程安全),也可以使用 Jedis。

不需要单独导入依赖。

配置序列化方式:(config包中)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 设置key和value的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置key的序列化器为StringRedisSerializer
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); // 设置value的序列化器为JdkSerializationRedisSerializer
        redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置hash key的序列化器为StringRedisSerializer
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer()); // 设置hash value的序列化器为JdkSerializationRedisSerializer
        redisTemplate.afterPropertiesSet(); // 初始化RedisTemplate
        return redisTemplate; // 返回配置好的RedisTemplate
    }
}

使用: 采用的方式是将redisf封装成redisTemplate,交给spring容器进行管理。使用的使用注入使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	// 注入 redisTemplate 
	private final StringRedisTemplate stringRedisTemplate;
    public UserServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
	
	@Test
    public void testSetGet() throws Exception {
        System.out.println("开始测试");
        // 使用 RedisTemplate 的 opsForValue() 方法获取操作字符串的命令对象
        stringRedisTemplate.opsForValue().set("name", "rose", Duration.of(10, ChronoUnit.SECONDS));
        // 从 Redis 中获取键为 name 的值,并将其存储在 name 变量中
        String name = stringRedisTemplate.opsForValue().get("name");
        System.out.println(name);
    }

swagger

离线接口文档, 也可以使用 Knife4j — > swagger2 + openapi3

这里使用Knife4J,整合了两者,swagger文档应该也是可以查看的。

对应版本的依赖:springboot3 需要使用 4.0.0 以上的。

1
2
3
4
5
         <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.4.0</version>
        </dependency>

配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  # 分组以及扫描的路径。
  group-configs:
    - group: 'user'
      paths-to-match: '/**'
      #生成文档所需的扫包路径,一般为启动类目录
      packages-to-scan: top.rose
 
#knife4j配置,不需要增强可以不配
knife4j:
  #是否启用增强设置
  enable: true
  #开启生产环境屏蔽
  production: false
  #是否启用登录认证
  basic:
    enable: true
    username: admin
    password: 123456
  setting:
    language: zh_cn
    enable-version: true
    enable-swagger-models: true
    swagger-model-name: 用户模块

使用OpenAPI3的规范注解,注解各个spring的REST接口

@Tag注解:标记接口类别

@Operation:标记接口操作

访问:http://ip:port/doc.html 即可查看。

这里要注意,不要和统一处理结果一并使用,否则会出现 knife4j 文档请求异常。

接下来配置以下接口文档的作者等信息,在config目录下新建配置类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package top.rose.template.commom.config;

/**
 * @ author: rose
 * @ date: 2024-12-02
 * @ version: 1.0
 * @ description:
 */
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 springShopOpenApi() {
        return new OpenAPI()
                // 接口文档标题
                .info(new Info().title("rose的demo")
                        // 接口文档简介
                        .description("这是基于Knife4j OpenApi3的测试接口文档")
                        // 接口文档版本
                        .version("1.0版本")
                        // 开发者联系方式
                        .contact(new Contact().name("rose")
                                .email("000000000@qq.com")));
    }

}

如果你有设置拦截器,请注意把对应的路径排除。


如果你仅仅想用swagger可以参考以下。

导入依赖,要和你的springboot 的版本兼容

最新的2.6.0版本,会自动加载,可以不需要手动写配置类。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.6.0</version>
</dependency>

如果你需要进行一些别的操作:比如配置api 的个性化描述等等,可以手动写配置类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@OpenAPIDefinition(
    info = @Info(
        title = "rose's Api",
        version = "1.0.0",
        description = "This is a sample API documentation",
        contact = @Contact(
            name = "Your Name",
            url = "http://yourwebsite.com",
            email = "youremail@example.com"
        ),
        license = @License(
            name = "Apache 2.0",
            url = "http://www.apache.org/licenses/LICENSE-2.0.html"
        )
    )
)
public class OpenApiConfig {
    // 这里可以添加其他配置
}

还有一些其他的功能:

  1. 配置扫描接口:指定扫描的包
  2. 控制swagger是否开启,用以解决只在生成环境中使用,发布了就关闭。
  3. 配置api分组
  4. 对api添加注释信息

在yml文件中配置:

1
2
3
4
5
6
# 要扫描的路径
springdoc:
  paths-to-match: /api/** 
# 扫描的包
springdoc:
  packages-to-scan: com.example.controller

如果您想要自定义扫描的路径,可以在配置类上使用 @OpenAPIDefinition 注解,并结合 @Tag 注解为特定的控制器或方法添加标签,从而更好地组织和展示 API 文档。

@Tag注解:标记接口类别

@Operation:标记接口操作

参数校验

常用于dto对象,和controller 层

spring boot注解化参数校验的初级使用

依赖:

1
2
3
4
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

常用校验规则:

  • @NotNull:用于标记字段或方法参数不能为空。非null
  • @NotEmpty:用于标记集合、数组、字符串不能为空。非空集合、数组、字符串
  • @NotBlank:用于标记字符串不能为空且长度必须大于0。非null且非空字符串
  • @Size:用于标记集合、数组、字符串长度必须在指定范围内。
  • @Min:用于标记数字类型的最小值。
  • @Max:用于标记数字类型的最大值。
  • @Email:用于标记字符串必须为邮箱格式。

@NotNull用于一般的非空校验,@NotEmpty用于集合、数组、字符串的非空校验,@NotBlank则用于字符串的非空校验且长度必须大于0。

@NotNull注解只要求不为null,无法处理空字符,空字符串在它这里是通过的。@NotBlank注解不仅要求不为null,还会要求去除前后空格后长度大于0,也就是它要求不能是空字符串。

@Validated

为dto对象属性添加规则,在controller开启校验@Validated注解或者@Valid注解来启用校验。在spring boot推荐用@Validated注解,因为它能够支持 Spring 提供的校验注解,并且具有更好的集成性。而@Valid 则是java标准库提供的。

但是仅仅会返回不友好的信息

image-20241202164638292
image-20241202164638292

可以在在全局异常处理中进行配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> handleValidationExceptions(Exception ex) {
        log.error(ex.getMessage());
        // 从异常中获取字段错误信息
        FieldError fieldError = ((MethodArgumentNotValidException) ex).getBindingResult().getFieldError();
        if (fieldError != null) {
            // 获取错误提示信息
            String errorMessage = fieldError.getDefaultMessage();
            log.error(errorMessage);
            return Result.error(errorMessage);
        } else {
            // 如果没有字段错误,返回默认错误信息
            log.error(ex.getMessage());
            return Result.error("请求参数验证失败");
        }
    }

image-20241202165057257
image-20241202165057257

认证鉴权 jwt

jwt令牌,JWT可以被用作身份验证和授权,通过在服务器和客户端之间传递令牌来验证用户的身份并允许访问受保护的资源。

由于JWT是基于数字签名的,所以可以确保数据的完整性和安全性。它的设计简单、易于实现,并且可以跨不同的平台和语言使用。

它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
 </dependency>
<!-- 如果jdk大于1.8,则还需导入下面依赖-->
 <dependency>
     <groupId>javax.xml.bind</groupId>
     <artifactId>jaxb-api</artifactId>
     <version>2.3.1</version>
 </dependency>

编写 jwt 工具类:

  • 生成JWT:用于生成JWT令牌并返回令牌字符串。

  • 解析JWT:用于解析JWT令牌,验证签名,并获取其中的声明信息。

  • 验证JWT:用于验证JWT的有效性,包括验证签名、过期时间等。

  • 刷新JWT:用户还在操作,马上要快过期时,延长其有效期(无感刷新)。

  • 其他辅助方法:例如获取JWT中的特定声明信息,验证JWT是否包含某个声明,等等。

正常和解析是必须的,其他可以根据需求来。由于在解析 jwt时,如果出现被篡改、过期的情况就会抛出异常。也算是验证jwt。可以用来登录。

通过类名.静态方法使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
/**
 * @author mijiupro
 */
public class JwtUtils {
 
    private  String SECRET_KEY;// 加密密钥
    private  long EXPIRE_TIME; //到期时间,12小时,单位毫秒
 
    /**
     * 生成token令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @param subject 主题(用户类型)
     * @return token
     */
    public static String generateToken(Map<String,Object> claims, String subject) {
        return Jwts.builder()
                .setId(Claims.ID)//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为令牌的唯一标识。
                .setSubject("mijiu")//设置主题,一般为用户类型
                .setIssuedAt(new Date())//设置签发时间
                .addClaims(claims)//设置负载
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)//设置签名算法
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))//设置令牌过期时间
                .compact();//生成令牌
    }
 
    /**
     * 解析token令牌
     * @param token token令牌
     * @return 负载
     */
    public static Claims parseToken(String token) {
 
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
    /**
     *  验证token令牌
     * @param token 令牌
     * @return 是否有效
     */
    public static boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
 
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 刷新Token
     * @param token 旧的Token令牌
     * @return 新的Token令牌
     */
    public static String refreshToken(String token) {
        try {
            // 解析旧的Token,获取负载信息
            Claims claims = parseToken(token);
            // 生成新的Token,设置过期时间和签名算法等参数
            return generateToken(claims, claims.getSubject());
        } catch (Exception e) {
            throw new RuntimeException("无法刷新令牌!", e);
        }
    }
    /**
     * 从令牌中获取主题信息
     * @param token 令牌
     * @return 主题信息(用户类型)
     */
    public static String getSubjectFromToken(String token) {
        try {
            Claims claims = parseToken(token); // 解析令牌,获取负载信息
            return claims.getSubject(); // 返回主题信息
        } catch (Exception e) {
            throw new RuntimeException("无法从令牌中获取主题。", e);
        }
    }
 
}

也可以将secret 设置到配置文件中。

基础功能完善

跨域问题

跨域问题是指在 Web 开发中,一个网页的 JavaScript 代码通过 AJAX 请求后端服务器接口时,如果请求的目标地址与当前页面的地址不在同一个域(域名端口协议任何一项不同),就会产生跨域问题。这种情况下,根据浏览器的安全机制(同源策略)就会会限制页面的跨域请求,以防止恶意网站对其他网站的访问和操作,保护用户信息安全。

解决:自定义WebConfigurer

通过实现 WebMvcConfigurer 接口来自定义 WebMvc 配置,并覆盖 addCorsMappings 方法以配置全局跨域规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 对所有路径生效
                .allowedOrigins("*") //允许所有源地址,也可以时域名数组。
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的请求方法
                .allowedHeaders("*"); // 允许的请求头
    }
}

还可以通过 @CrossOrigin(origins = "*")注解来允许跨域,以及自定义过滤器去实现。

拦截器 !!!!

拦截没有对应权限的用户进行访问,也就是实现权限控制。

拦截器流程:

用户登录后服务器会发放token等信息(jwt)返回给前端,前端进行保存(header中),再携带着该token 访问服务器,首先会被拦截,只有校验通过后,才能正确的返回对应的资源,否则会抛出异常。

tips:

  • 每次登录都创建token
  • 用户访问期间token 不能失效,需要及时刷新 —– 》 通过每次访问接口,就刷新token来解决。
  • token 被正确解析后,根据解析的内容,查询对应的数据。

实现步骤:(核心)

定义拦截器 —- 创建拦截器配置类 —- 配置拦截器顺序 —- 配置拦截器排除项。

1 定义拦截器

为了解决用户访问期间token 不会失效,为其设置一个专门刷新token的拦截器 RefreshTokenInterceptor

需要注意的是:不是拦截所有的访问都需要token,比如一些没有登录浏览的页面等等,如果没有token就直接放行,有token就刷新。最后请求处理完一定要清理一下本地线程,不然用户多的时候内存占用会很大。

需要实现 Spring 框架提供的 HandlerInterceptor 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Component
@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {

    // redis 和 jwt 的使用

    public final StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1 判断是否有token,没有token 就放行
        String authorizationHeader = request.getHeader("authorization");
        if(StringUtils.isBlank(authorizationHeader))
        {
             return true;
        }
        // 2 解析token  --- 这里如果解析出错怎么办????
        Claims claims = JwtUtils.parseToken(authorizationHeader);
        if(Objects.isNull(claims)) return true;
        // 3 获取用户信息
        Integer userId = claims.get("userId",Integer.class);
        //  从redis中获取信息。
        String userInfoJson = stringRedisTemplate.opsForValue().get("login:user:"+userId);
        if(StringUtils.isBlank(userInfoJson)) return true;

        // 4 刷新token
        String refreshToken  = JwtUtils.refreshToken(authorizationHeader);
        response.setHeader("Access-Control-Expose-Headers","Authorization");
        response.setHeader("Authorization",refreshToken);
        stringRedisTemplate.expire("login:user:"+userId,30, TimeUnit.MINUTES);
        // 5 将用户信息存储到本地线程
        User user = JSONUtil.toBean(userInfoJson, User.class);
        //  这个再具体看模板       UserHolder
        UserHolder.setInfoByToken(user);

        return true;
    }

    // 执行后清理一下本地线程
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.clear();
    }
}

除此还需要一个身份认证的拦截器,用以身份认证。LoginInterceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 在经过该拦截器前会有token判断是否存在,因此只需要判断本地线程中是否有用户信息
        log.info("进入登录拦截器");
        User user = UserHolder.getInfoByToken();
        if(Objects.isNull(user)){
            log.info("过期异常!");
            throw new TokenOverdueException(ResultEnum.PERMISSION_EXPIRE);
        }
        log.info("放行");
        return true;
    }
}
创建拦截器配置类

创建一个配置类,用于配置拦截器链。在该配置类中,通过实现 WebMvcConfigurer 接口来添加拦截器,具体包括 addInterceptors 方法。(和上文的解决跨域问题的配置类一起)

配置拦截器顺序:通过 .order(0) 进行配置拦截器的顺序

配置拦截器排除项 .excludePathPatterns()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Configuration
public class WebConfig implements WebMvcConfigurer {


    // 拦截器配置
    private final RefreshTokenInterceptor refreshTokenInterceptor;
    private final LoginInterceptor loginInterceptor;

    public WebConfig(RefreshTokenInterceptor refreshTokenInterceptor, LoginInterceptor loginInterceptor) {
        this.refreshTokenInterceptor = refreshTokenInterceptor;
        this.loginInterceptor = loginInterceptor;
    }

    public void addInterceptors(InterceptorRegistry registry)
    {
        // 对所有路劲进行拦截,顺序为 0
        registry.addInterceptor(refreshTokenInterceptor)
                .addPathPatterns("/**").order(0);
        
        // 登录拦截器,拦截除登录注册以及 swagger 的路径,以及一些静态资源
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns("/","*/login")
                .order(1);
    }
    
    // 跨域问题解决
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 对所有路径生效
                .allowedOrigins("*") //允许所有源地址
                // .allowedOrigins("https://mijiupro.com","https://mijiu.com ") // 允许的源地址(数组)
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的请求方法
                .allowedHeaders("*"); // 允许的请求头
    }
}

知道其流程:

登录后将用户信息存储到redis中,设置有效期30分钟。

然后 将userid 放到jwt中添加到前端中 authorization字段中。

author: rose
Built with Hugo
Theme Stack designed by Jimmy
© Licensed Under CC BY-NC-SA 4.0