【SpringBoot】validation参数校验 && JWT鉴权实现 && 加密/加盐
摘要 本文介绍了Jakarta Bean Validation参数校验和JWT令牌技术两大主题。参数校验部分讲解了基于注解的校验方式,列举了常见校验注解(如@NotBlank、@Email等)及在Spring中的使用方法。JWT部分分析了传统登录方式的问题,详细解释了JWT的组成结构(Header、Payload、Signature)及其优势,并提供了完整的JWT实现方案,包括依赖配置、工具类编写
文章目录
参数校验: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
- 官网:https://jwt.io/
- 是一种紧凑的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("密码不正确");
}

更多推荐


所有评论(0)