参数校验: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) | 最大值(适用于数字) |
| 字符串必须为邮箱格式 | |
| @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令牌技术解决方案
令牌其实就是一个用户身份的标识,本质就是一个字符串 。
服务器只需要存放一份密钥来判断 token 中 payload 部分是否发生变化(有点像证书机制),而不需要像 session 机制那样存放大量的 session 字符串,大大节省存储空间~
令牌技术优点
-
解决了集群环境下的认证问题
-
减轻服务器的存储压力(无需在服务器端存储)
JWT介绍
-
JWT全称:JSON Web Token
-
是一种紧凑的URL安全方法,用于客户端和服务器之间传递安全可靠的信息
JWT组成
JWT 由三部分组成,每部分中间使用 . 分隔,如:aaaaa.bbbbb.cccc
-
Header(头部) :包括令牌类型及使用的哈希算法 -
Payload(载荷) :存放有效信息的地方,如用户ID、用户名、过期时间戳等 -
Signature(签名) :将 头部+载荷 结合 **密钥 ** 进行加密,用于防止JWT内容被篡改。- 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("密码不正确");
}