Files
number/后端架构设计/11-通用基础设施-part2-JWT与拦截器.md
2026-03-17 12:09:43 +08:00

4.6 KiB
Raw Permalink Blame History

通用基础设施开发文档 - Part 2JWT + UserContext + 拦截器)


一、JwtUtil.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);
    }
}
# application.yml
jwt:
  secret: change-this-to-a-256-bit-random-secret-in-prod
  expire-ms: 86400000   # 24 小时

二、UserContext.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

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注册拦截器

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