在这里插入图片描述

参数校验:jakarta.validation

Jakarta Bean Validation 提供了一种 基于注解的方式 来验证 Java 对象中的属性是否符合规则,通常用于:

  • 表单输入校验(Web 开发)
  • DTO 参数校验(SpringMVC、Jakarta REST)
  • 持久化数据校验(JPA)

SpringBoot 项目使用时,添加以下依赖即可:

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

常见注解

注解 含义
@NotNull 字段不能为 null
@NotBlank 字符串不为 null 且去除空格后长度大于0
@NotEmpty 集合、数组或字符串不为 null 且不为空
@Size(min, max) 长度或元素个数在一定范围内
@Min(value) 最小值(适用于数字)
@Max(value) 最大值(适用于数字)
@Email 字符串必须为邮箱格式
@Pattern(regexp) 正则表达式匹配
@Past / @Future 时间必须是过去/未来

使用实例

public class User {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Size(min = 6, message = "密码长度不能少于6位")
    private String password;

    @Min(value = 18, message = "年龄不能小于18岁")
    private Integer age;

    // getters and setters
}

如何触发验证?

Spring 框架中,配合 @Valid@Validated 注解使用:

@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody User user, BindingResult result) {
    if (result.hasErrors()) {
        return ResponseEntity.badRequest().body(result.getAllErrors());
    }
    return ResponseEntity.ok("注册成功");
}

JWT

1. 传统登录方式的问题

传统的登录认证流程通常是:

  • 用户提交用户名密码到服务器
  • 服务器验证身份并创建 Session
  • 服务器通过 Cookie 返回 sessionId 给浏览器

但在集群环境下,这种方式存在问题:

  • 单点故障风险高
  • 多服务器环境下,一个用户的请求可能被分发到不同服务器
  • 第一台服务器创建的 Session 在第二台服务器上不存在,导致用户需要重复登录

2. JWT令牌技术解决方案

令牌其实就是一个用户身份的标识,本质就是一个字符串

服务器只需要存放一份密钥来判断 tokenpayload 部分是否发生变化(有点像证书机制),而不需要像 session 机制那样存放大量的 session 字符串,大大节省存储空间~

令牌技术优点

  • 解决了集群环境下的认证问题
  • 减轻服务器的存储压力(无需在服务器端存储)

JWT介绍

  • JWT全称:JSON Web Token
  • 官网:https://jwt.io/
  • 是一种紧凑的URL安全方法,用于客户端和服务器之间传递安全可靠的信息

JWT组成

JWT` 由三部分组成,每部分中间使用 . 分隔,如:aaaaa.bbbbb.cccc
  1. Header(头部):包括令牌类型及使用的哈希算法
  2. Payload(载荷):存放有效信息的地方,如用户ID、用户名、过期时间戳等
  3. Signature(签名):将 头部+载荷 结合 密钥 进行加密,用于防止 JWT 内容被篡改。
    1. JWT 解决的是 “信任” 问题,而不是 “隐私” 问题,即 JWT 并没有办法保证数据内容的安全性,所以不要在载荷中存放敏感信息

所有部分使用 Base64Url 编码(注意:Base64 是编码方式,不是加密方式)

3. 实现JWT登录认证

3.1 添加JWT依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

然后在配置文件中添加密钥(这里采用配置文件引入的方式)

jwt.secret=wFApjmSTFmWZZix27k/w5ltH3YK9u3/e01IdCNsZ4Jk=

3.2 创建JWT工具类

@Slf4j
public class JwtUtil {
    // 没办法直接调用非静态变量secret
    // 所以换个思路,用传参方式来进行初始化
    // 即创建配置类调用init()来进行SECRET_KEY的初始化
    private static Key SECRET_KEY;

    // 由配置类主动调用初始化,对secret进行解码,然后转化为Key类型
    public static void init(String secret) {
        log.info("初始化密钥:{}", secret);
        SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
    }

    /**
     * 根据传入的claims也就是载荷,生成对应的JWT
     */
    public static String createJWT(Map<String, Object> claims) {
        if (SECRET_KEY == null) {
            throw new IllegalStateException("SECRET_KEY 未初始化!");
        }

        String jwt = null;
        try {
            jwt = Jwts.builder()
                    .setClaims(claims)
                    .signWith(SECRET_KEY, io.jsonwebtoken.SignatureAlgorithm.HS256)
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + Constants.TOKEN_EXPIRE_TIME)) // 1小时有效
                    .compact(); // 👈 核心!将 header + payload + signature 拼接、压缩、编码成一个标准的 JWT 字符串。
        } catch (Exception e) {
            throw new JwtException("创建令牌出错", e);
        }
        return jwt;
    }

    /**
     * 将生成JWT字符串解析后进行返回
     */
    public static Claims parseJWT(String jwt) {
        if (SECRET_KEY == null) {
            throw new IllegalStateException("SECRET_KEY 未初始化!");
        }
        if(!StringUtils.hasText(jwt)) {
            throw new IllegalArgumentException("JWT参数错误!");
        }

        Claims claim = null;
        try {
            claim = (Claims) Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody();

            // ✅ 检查是否过期
            if (claim.getExpiration().before(new Date())) {
                throw new RuntimeException("Token 已过期");
            }
            return claim;

        } catch (ExpiredJwtException e) {
            throw new JwtException("Token 已过期", e);
        } catch (JwtException e) {
            throw new JwtException("Token 非法", e);
        } catch (Exception e) {
            throw new JwtException("解析令牌出错", e);
        }
    }
}

3.3 创建配置类

这个配置类就是用于初始化上面 JWTUtils 中的 SECRET_KEY 密钥的。

@Slf4j
@Component
public class JWTConfig {
    @Value("${jwt.secret}")
    private String secret; // 不能为static,否则注入不成功,直接为null

    //该方法在注入secret后才执行
    @PostConstruct
    public void init() {
        log.info("【JWTUtils】PostConstruct 正在执行...");
        JWTUtils.init(secret); // 调用工具类的初始化方法
    }
}

3.4 前端实现的细节

前端想在页面跳转后还能用 token 进行验证,那么就得用 localStorage.setItem() 进行存储,然后需要用到的时候就用 localStorage.getItem() 获取即可!

function login() {
    $.ajax({
        type: "post",
        url: "/user/login",
        contentType:"application/json",
        data: JSON.stringify({
            "userName": $("#username").val(),
            "password": $("#password").val()
        }),
        success: function (result) {
            if (result.code == 200 && result.data != "") {
                var response = result.data;
                localStorage.setItem("user_token", response.token);
                localStorage.setItem("loginUserId", response.userId);
                location.assign("blog_list.html");
            } else {
                alert("用户名或密码错误");
                return;
            }
        }
    });
}

然后在前端统一处理部分,每次访问新页面的时候,就设置请求,发送该 token 给后端进行校验,如下所示:

$(document).ajaxSend(function (e, xhr, opt) {
    var user_token = localStorage.getItem("user_token");
    xhr.setRequestHeader("user_token", user_token);
});

4. Auth0 提供的 JWT

JWT 是 Auth0 提供的库类(包名是 com.auth0.jwt),而前面的 Jwts 是 JJWT 库的工具类(包名是 io.jsonwebtoken)。

这个包实现 JWT 会更加简洁一些

先引入依赖:

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.x.x</version>
</dependency>

然后编写工具类

/**
 * JWT 工具类
 */
public class JwtUtil {

    // 密钥
    private static final String SECRET_KEY = "xxx"; // 更改为你的密钥
    // 设置 JWT 的过期时间 6 小时
    private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 6;

    /**
     * 生成 JWT token
     *
     * @param claims 自定义的业务数据
     * @return JWT token
     */
    public static String generateToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims) // 自定义的业务数据
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间
                .sign(Algorithm.HMAC256(SECRET_KEY)); // 使用 HMAC256 算法加密
    }

    /**
     * 解析 JWT token
     *
     * @param token JWT token
     * @return 自定义的业务数据
     */
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET_KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }
}

加密/加盐

1. 密码加密方案

博客系统中采用 MD5算法 + 盐值 进行密码加密:

  • 使用随机字符串作为 “盐”
  • 将盐与密码组合后进行 MD5 加密
  • 存储格式为:盐值 + MD5(盐值+密码)

在这里插入图片描述

其中 “盐值” 是指一个随机字符串。

2. 实现加密工具类

public class SecureUtils {
    public static String encrypt(String passwd) {
        // 1. 获取盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        
        // 2. 获取 "盐值+密码" 进行md5加密后的密文
        String ret = DigestUtils.md5DigestAsHex((salt + passwd).getBytes(StandardCharsets.UTF_8));
        
        // 3. 返回真正的密码是:盐值 + 加密后的密文
        return salt + ret;
    }

    public static Boolean isValidated(String ciphertext, String passwd) {
        if(!StringUtils.hasLength(passwd) || !StringUtils.hasLength(ciphertext)) {
            return false;
        }
        if(ciphertext.length() != 64) {
            return false;
        }

        String salt = ciphertext.substring(0, 32); // 拿到盐值
        String tmp = DigestUtils.md5DigestAsHex((salt + passwd).getBytes(StandardCharsets.UTF_8));
        return (salt + tmp).equals(ciphertext);
    }
}

3. 修改登录验证逻辑

// login...

// 走到这说明用户存在,则进行密码判断
if(SecureUtils.isValidated(userInfo.getPassword(), password)) {
    // 验证成功...
} else {
    throw new BlogException("密码不正确");
}

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐