175 lines
4.6 KiB
Markdown
175 lines
4.6 KiB
Markdown
|
|
# 通用基础设施开发文档 - Part 2(JWT + UserContext + 拦截器)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、JwtUtil.java
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.util;
|
|||
|
|
|
|||
|
|
import io.jsonwebtoken.*;
|
|||
|
|
import io.jsonwebtoken.security.Keys;
|
|||
|
|
import org.springframework.beans.factory.annotation.Value;
|
|||
|
|
import org.springframework.stereotype.Component;
|
|||
|
|
import java.security.Key;
|
|||
|
|
import java.util.Date;
|
|||
|
|
|
|||
|
|
@Component
|
|||
|
|
public class JwtUtil {
|
|||
|
|
|
|||
|
|
private final Key key;
|
|||
|
|
private final long expireMs;
|
|||
|
|
|
|||
|
|
public JwtUtil(
|
|||
|
|
@Value("${jwt.secret}") String secret,
|
|||
|
|
@Value("${jwt.expire-ms}") long expireMs) {
|
|||
|
|
this.key = Keys.hmacShaKeyFor(secret.getBytes());
|
|||
|
|
this.expireMs = expireMs;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public String generate(Long userId, String role) {
|
|||
|
|
return Jwts.builder()
|
|||
|
|
.setSubject(userId.toString())
|
|||
|
|
.claim("role", role)
|
|||
|
|
.setIssuedAt(new Date())
|
|||
|
|
.setExpiration(new Date(System.currentTimeMillis() + expireMs))
|
|||
|
|
.signWith(key, SignatureAlgorithm.HS256)
|
|||
|
|
.compact();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public Claims parse(String token) {
|
|||
|
|
return Jwts.parserBuilder()
|
|||
|
|
.setSigningKey(key).build()
|
|||
|
|
.parseClaimsJws(token)
|
|||
|
|
.getBody();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public Long getUserId(String token) {
|
|||
|
|
return Long.parseLong(parse(token).getSubject());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public String getRole(String token) {
|
|||
|
|
return parse(token).get("role", String.class);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# application.yml
|
|||
|
|
jwt:
|
|||
|
|
secret: change-this-to-a-256-bit-random-secret-in-prod
|
|||
|
|
expire-ms: 86400000 # 24 小时
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、UserContext.java
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.util;
|
|||
|
|
|
|||
|
|
public class UserContext {
|
|||
|
|
|
|||
|
|
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
|
|||
|
|
private static final ThreadLocal<String> ROLE = new ThreadLocal<>();
|
|||
|
|
|
|||
|
|
public static void set(Long userId, String role) {
|
|||
|
|
USER_ID.set(userId);
|
|||
|
|
ROLE.set(role);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Long getUserId() { return USER_ID.get(); }
|
|||
|
|
public static String getRole() { return ROLE.get(); }
|
|||
|
|
|
|||
|
|
public static void clear() {
|
|||
|
|
USER_ID.remove();
|
|||
|
|
ROLE.remove();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、AuthInterceptor.java
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.interceptor;
|
|||
|
|
|
|||
|
|
import com.openclaw.constant.ErrorCode;
|
|||
|
|
import com.openclaw.exception.BusinessException;
|
|||
|
|
import com.openclaw.util.JwtUtil;
|
|||
|
|
import com.openclaw.util.UserContext;
|
|||
|
|
import jakarta.servlet.http.*;
|
|||
|
|
import lombok.RequiredArgsConstructor;
|
|||
|
|
import org.springframework.stereotype.Component;
|
|||
|
|
import org.springframework.web.servlet.HandlerInterceptor;
|
|||
|
|
|
|||
|
|
@Component
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class AuthInterceptor implements HandlerInterceptor {
|
|||
|
|
|
|||
|
|
private final JwtUtil jwtUtil;
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public boolean preHandle(HttpServletRequest req,
|
|||
|
|
HttpServletResponse res,
|
|||
|
|
Object handler) {
|
|||
|
|
String auth = req.getHeader("Authorization");
|
|||
|
|
if (auth == null || !auth.startsWith("Bearer "))
|
|||
|
|
throw new BusinessException(ErrorCode.UNAUTHORIZED);
|
|||
|
|
try {
|
|||
|
|
String token = auth.substring(7);
|
|||
|
|
Long userId = jwtUtil.getUserId(token);
|
|||
|
|
String role = jwtUtil.getRole(token);
|
|||
|
|
UserContext.set(userId, role);
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
throw new BusinessException(ErrorCode.UNAUTHORIZED);
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
|
|||
|
|
Object handler, Exception ex) {
|
|||
|
|
UserContext.clear(); // 防止 ThreadLocal 内存泄漏
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、WebMvcConfig.java(注册拦截器)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.config;
|
|||
|
|
|
|||
|
|
import com.openclaw.interceptor.AuthInterceptor;
|
|||
|
|
import lombok.RequiredArgsConstructor;
|
|||
|
|
import org.springframework.context.annotation.Configuration;
|
|||
|
|
import org.springframework.web.servlet.config.annotation.*;
|
|||
|
|
|
|||
|
|
@Configuration
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class WebMvcConfig implements WebMvcConfigurer {
|
|||
|
|
|
|||
|
|
private final AuthInterceptor authInterceptor;
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void addInterceptors(InterceptorRegistry registry) {
|
|||
|
|
registry.addInterceptor(authInterceptor)
|
|||
|
|
.addPathPatterns("/api/**")
|
|||
|
|
.excludePathPatterns(
|
|||
|
|
"/api/v1/users/register",
|
|||
|
|
"/api/v1/users/login",
|
|||
|
|
"/api/v1/users/sms-code",
|
|||
|
|
"/api/v1/payments/callback/**",
|
|||
|
|
"/api/v1/skills", // 公开浏览
|
|||
|
|
"/api/v1/skills/{id}" // 公开详情
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
**文档版本**:v1.0 | **创建日期**:2026-03-16
|