Jwt 如何在 springboot 项目中进行接口访问鉴权

本文详细介绍了如何在SpringBoot项目中使用JWT进行token验证,包括拦截器的使用、JWT的生成与校验、处理错误路径以及自定义token验证流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

结合以下文章:
jwt.io 官网详细介绍

SpringBoot集成JWT实现Token登录验证

SpringBoot项目使用JWT+拦截器实现token验证

spring-boot + JWT实现TOKEN登录接口验证

SpringBoot集成JWT实现token验证

SpringBoot 开发 – JWT 认证教程

1 springboot 框架负责接口的拦截和放行

1.1 原理

使用 HandlerInterceptor (对于 springboot 框架不推荐使用 doFilter)

1.2 思路

白名单思路, 拦截所有接口目录/**, 放行需要的接口

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
}

1.3 坑: Springboot 访问了错误处理路径 /error

接口程序中有未处理的异常, 报了 Null Pointer Exception, Springboot 调用默认的错误处理接口 /error 企图调用错误处理程序, 第二次触发了 HandlerInterceptor, 由于/error不带 token, 所以被拒绝,最终报 token 校验不通过的错误信息.

这里的解决方法:

1 处理程序中所有异常, 在最外层捕捉不可预见的异常, 返回统一错误信息,服务内部错误
2 自定义 springboot 的 error path 为符合自己程序的路径, 并用 controller 定义处理程序. 当springbot框架本身或者依赖包出现不可预知的错误时,转到这里, 可以返回统一错误信息

其它方法也可以尝试使用 @ControllerAdvice 自定义异常处理类, 处理程序自身不可预知的错误

2 jwt token 负责携带数据和签名的生成及校验

官方库

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

2.1 初始化

JWTCreator.Builder builder = JWT.create();

2.2 设置 Header

Map<String,Object> headerMap = new HashMap<>();
builder.withHeader(headerMap);

2.3 携带数据 payload

自定义数据

for (Map.Entry<String,String> entry:data.entrySet()) {
    builder.withClaim(entry.getKey(), entry.getValue());
}

设置过期时间

builder.withExpiresAt(expireDate);

Token 放在请求Header的Authorization字段里。Token 携带数据userId

Token 的格式:

header

{
  "kid": "XXXXXXXXXXXXXXXXXX0MDVmLWIyMjEtMjQ1MWU3NWYxXXXXX5",
  "typ": "JWT",
  "alg": "RS256"
}

payload

{
  "exp": 1684829637,
  "userId": "xxxxxxxxxxxx==",
  "iat": 1684829607
}

2.4 签名 sign 后, 生成 token

token = builder.sign(Algorithm.HMAC256(secretKey))

如果使用RSA非对称算法,可以从jwt库的源码看出使用私钥签名

token = builder.sign(Algorithm.RSA256(rsaPrivateKey))

java-jwt-4.0.0-sources.jar!/com/auth0/jwt/algorithm/RSAAlgorithm.java

    @Override
    public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
        try {
            RSAPrivateKey privateKey = keyProvider.getPrivateKey();
            if (privateKey == null) {
                throw new IllegalStateException("The given Private Key is null.");
            }
            return crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes);
        } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) {
            throw new SignatureGenerationException(this, e);
        }
    }

2.5 校验

配置算法

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secretKey)).build();

如果使用RSA非对称算法,可以从jwt库的源码看出使用公钥校验

JWTVerifier verifier = JWT.require(Algorithm.RSA256(rsaPublicKey)).build();

java-jwt-4.0.0-sources.jar!/com/auth0/jwt/algorithm/RSAAlgorithm.java

    @Override
    public void verify(DecodedJWT jwt) throws SignatureVerificationException {
        try {
            byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature());
            RSAPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId());
            if (publicKey == null) {
                throw new IllegalStateException("The given Public Key is null.");
            }
            boolean valid = crypto.verifySignatureFor(
                    getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes);
            if (!valid) {
                throw new SignatureVerificationException(this);
            }
        } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException
                | IllegalArgumentException | IllegalStateException e) {
            throw new SignatureVerificationException(this, e);
        }
    }

校验

DecodedJWT decodedJWT  = verifier.verify(token);

校验的方法是再生成一遍进行比较

2.6 获取信息

两种方法

  • 第一种方法: 在 HandlerInterceptor 里的 PreHandle 校验通过后, 立即解析 token, 拿到数据. 把解析结果放入 threadlocal 变量, 这样在整个请求的主线程里, 可以使用该变量, 并且该变量对其它线程不可见, 在请求结束的 afterCompletion() 方法里把 threadlocal 变量注销释放.
  • 第二种方法: 在需要获取信息的时候, 先获取该severlet请求的上下文 RequestContextHolder, 进而拿到请求Request中的 header, 进而拿到 token, 重新解析 token, 获取数据. 由于接口进来时, 已经通过校验, 可以不通过校验的方式获取解析后的token, 直接调用解析方法进行解析即可.

2.7 字段说明

nbf 可用于多机部署时, 服务器之间时间的微小差异

3 拦截器代码

Springboot 建议使用HandlerInterceptor进行拦截

定义 annotation, 对这个annotation 修饰的接口进行拦截


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessWithoutToken {
    boolean required() default true;
}

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.info("Request from {} to URI: {}, URL: {}", HttpClientUtil.getRemoteIp(request), request.getRequestURI(), request.getRequestURL().toString());
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        log.info("Method {}, {}", method.getName(), method.getDeclaredAnnotations());
        if (method.isAnnotationPresent(AccessWithoutToken.class)) {
            AccessWithoutToken accessWithoutToken = method.getAnnotation(AccessWithoutToken.class);
            if (accessWithoutToken.required()) {
                return true;
            }
        }

        // Authorization: Bearer <token>
        String authorization = request.getHeader("Authorization");

        // TODO check token
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        if (!org.apache.commons.lang3.StringUtils.isBlank(authorization)) {

            String[] authorizationStr= StringUtils.split(authorization, SPACE);
            if (2 == authorizationStr.length) {
                String authType = authorizationStr[0];
                String token = authorizationStr[1];

                if (authType.equals("Bearer") && !org.apache.commons.lang3.StringUtils.isBlank(token)) {

                    DecodedJWT decodedJWT = JwtUtil.verifyToken(token);
                    if (null != decodedJWT) {
                        // TODO 校验通过获取信息
                        log.info("token: {}......, 校验通过, 签发时间{}, userId{}", token.substring(0, 32), decodedJWT.getIssuedAt().getTime(), decodedJWT.getClaim("userId"));
                        return true;
                    }
                } else {
                    log.error("Token 校验失败, auth prefix={}, token={}", authType, token);
                }
            } else {
                log.error("Token 校验失败, http header 中解析 Authorization 字段错误, authorization={}", authorization);
            }

        } else {
            log.error("Token 校验失败, http header 中没有 Authorization 字段, authorization={}", authorization);
        }
        try (PrintWriter writer = response.getWriter()) {
            writer.print(RestResponse.fail(RestCode.USER_VALIDATE_FAIL_JWT_TOKEN));
        } catch (Exception e) {
            log.error("登录 JWT Token 校验失败 未知错误 error=", e);
        }

        return false;
    }
}

扩展阅读

OWASP Top Ten 2021 : Related Cheat Sheets

okta What-is-the-lifetime-of-the-JWT-tokens

其它

关于springboot 默认 error path

customize springboot default error path
ErrorMvcAutoConfiguration.java
get-started-with-custom-error-handling-in-spring-boot-java/
spring-boot-custom-error-page
how-to-fix-spring-boot-customize-http-error-response-in-java
howto.actuator.customize-whitelabel-error-page
boot-features-error-handling

<think>首先,用户的问题是:“我想了解如何在SpringBoot中实现对外部接口访问本系统的认证请问springboot实现外部接口访问时的认证机制”。所以,我需要专注于如何在SpringBoot中处理外部接口访问时的认证。用户提到了“外部接口访问本系统”,意思是外部系统或客户端访问我的SpringBoot应用的接口时,如何进行认证。这涉及API安全,常见于微服务或WebAPI场景。关键点是:实现认证机制,确保只有授的客户端才能访问API。现在,回顾系统级指令:1.所有行内数学表达式用`$...$`格式。但在这个上下文中,可能没有数学表达式,所以我可以忽略。2.独立公式用`$$...$$`,但同样,可能不适用。3.LaTeX语法正确:确保任何数学表达正确。4.使用中文回答:整个回答用中文。5.生成相关问题:在回答结束后,添加相关问题部分。6.回答中引用的段落末尾自然地添加引用标识,如`[^1]`。引用的内容来自提供的引用。提供的引用:-[^1]:关于SpringBoot实现用户认证和授的基础知识。-[^2]:SpringBoot整合SpringSecurity的具体步骤。-[^3]:SpringSecurity的注解如`@PreAuthorize`,`@AuthenticationPrincipal`的使用。-[^4]:使用SpringBootGateway跳过某些服务,涉及过滤器。用户的问题是关于外部接口访问,所以引用[^2]和[^3]尤其相关,因为它们直接讨论SpringSecurity。引用[^4]涉及网关,可能用于API网关场景。核心机制:在SpringBoot中,实现API通常使用SpringSecurity。常见方式包括:-**基本认证(BasicAuthentication)**:用户名和密码通过HTTP头传递。-**JWT(JSONWebTokens)**:Token-based认证,客户端在请求头中包含Token。-**OAuth2**:用于授,支持客户端凭据流(clientcredentialsflow)适合外部系统。用户说“外部接口访问”,可能指其他服务或应用程序访问本系统,所以OAuth2客户端凭据或API密钥可能合适。在回答中,我需要结构清晰,逐步解决问题:1.**介绍机制**:解释什么是认证,为什么重要。2.**选择合适的方式**:基于SpringSecurity,讨论选项如基本认证、JWT、OAuth2。3.**实现步骤**:提供代码示例或关键配置。4.**引用支持**:在相关部分添加引用标识。最后,生成相关问题。结构化回答:-**引言**:简要解释问题。-**主要机制**:讨论SpringSecurity如何实现。-**具体实现**:提供示例代码或配置。-**总结**:强调最佳实践。-**相关问题**:添加2-3个相关问题。确保回答真实可靠:基于SpringBootSpringSecurity的官方文档或常见实践。使用引用:例如,当提到整合SpringSecurity时,引用[^2];当讨论注解时,引用[^3]。代码示例:如果需要,提供简单的代码片段,但用户问题可能不需要完整代码,除非指定。用户提到“外部接口访问”,所以焦点在API安全。可能的机制:1.**API密钥**:客户端在请求头中包含API密钥。2.**JWT**:客户端提供JWTtoken。3.**OAuth2客户端凭据**:客户端使用client_id和client_secret获取token。在SpringBoot中使用SpringSecurity实现。</think>### Spring Boot 实现外部接口访问认证机制 在 Spring Boot 中,实现外部接口访问认证机制是确保系统安全的关键一环。外部接口访问通常指其他系统、服务或客户端(如第三方应用、微服务)通过 API 调用您的 Spring Boot 应用时,需要进行身份验证和授。核心目标是防止未授访问,保护敏感数据Spring Boot 结合 Spring Security 提供了灵活的解决方案,支持多种认证方式,如 API 密钥、JWT(JSON Web Tokens)和 OAuth2。下面我将逐步解释机制、选择方案和实现步骤,确保内容结构清晰、真实可靠。 #### 1. **认证机制概述** - **为什么重要**:外部接口访问涉及跨系统交互,未经验证的请求可能导致数据泄露或攻击。认证通过验证客户端身份(认证)和检查访问限(授)来保障安全[^1]。 - **核心组件**: - **认证 (Authentication)**:确认客户端身份,例如通过 API 密钥或 Token。 - **授 (Authorization)**:检查客户端是否有执行操作,例如基于角色或范围。 - **常见方式**: - **API 密钥**:客户端在 HTTP 头(如 `X-API-KEY`)中传递密钥,简单易用,适用于低安全需求场景。 - **JWT**:使用 Token-based 认证,客户端提供由服务器签发的 JWT Token,支持无状态和分布式系统。 - **OAuth2 客户端凭据流**:适合机器间通信(如微服务),客户端使用 `client_id` 和 `client_secret` 获取 Token,然后访问资源。 Spring Security 是 Spring Boot 的官方安全框架,可轻松集成这些机制[^2]。 #### 2. **选择适合的方案** 根据安全需求和场景,推荐以下方案: - **简单场景(如内部工具)**:使用 API 密钥或基本认证(Basic Auth),配置快速。 - **中等安全需求(如外部 API 调用)**:JWT 更佳,支持 Token 过期和签名验证。 - **高安全需求(如云服务或开放平台)**:OAuth2 客户端凭据流,提供标准化授流程。 引用[^3] 强调了 Spring Security 的注解(如 `@PreAuthorize`)在授中的重要性,确保业务方法的安全。 #### 3. **实现步骤与示例代码** 以 **JWT 机制**为例,这是外部接口访问的常见选择,因为它无状态、可扩展。以下是基于 Spring BootSpring Security 的实现步骤。完整代码可参考官方文档或示例项目。 **步骤 1: 添加依赖** 在 `pom.xml` 中添加 Spring Security 和 JWT 相关依赖: ```xml <dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 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> </dependencies> ``` 这一步确保框架支持安全功能和 JWT 处理[^2]。 **步骤 2: 配置 Spring Security** 创建安全配置类,定义认证和授规则。使用 `@EnableWebSecurity` 启用安全功能,并配置 JWT 过滤器: ```java import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 禁用 CSRF,适合 API 场景 .authorizeRequests() .antMatchers("/api/public/**").permitAll() // 公共接口无需认证 .anyRequest().authenticated() // 其他接口需要认证 .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 过滤器 } // 自定义 JWT 过滤器 public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } } ``` 这里,`antMatchers` 用于定义 URL 路径的访问规则,`.authenticated()` 要求所有外部接口访问必须认证[^3]。 **步骤 3: 实现 JWT 认证过滤器** 创建过滤器类,用于验证请求头中的 JWT Token: ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final String SECRET_KEY = "your-secret-key"; // 替换为实际密钥 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader("Authorization"); // 从请求头获取 Token if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); // 去掉 "Bearer " 前缀 try { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); // 解析 Token String clientId = claims.getSubject(); // 获取客户端 ID // 创建认证对象 UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( clientId, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth); // 设置认证上下文 } catch (Exception e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); return; } } chain.doFilter(request, response); // 继续过滤器链 } } ``` 此过滤器检查 `Authorization` 头中的 Token,验证其有效性。无效 Token 返回 401 错误[^3]。 **步骤 4: 生成和颁发 JWT Token** 为外部客户端提供获取 Token 的接口。例如,创建一个控制器用于 Token 颁发: ```java import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.security.Key; import java.util.Date; @RestController public class AuthController { private final Key SECRET_KEY = Keys.hmacShaKeyFor("your-secret-key".getBytes()); // 与过滤器密钥一致 @PostMapping("/api/token") public String generateToken(@RequestParam String clientId) { long expirationTime = 3600000; // Token 有效期为 1 小时 return Jwts.builder() .setSubject(clientId) // 客户端标识 .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(SECRET_KEY) .compact(); // 生成 Token } } ``` 外部客户端先调用此接口获取 Token(需提供 `clientId`),然后在后续请求头中加入 `Authorization: Bearer <token>`。 **步骤 5: 授检查** 在业务接口上使用 Spring Security 注解进行细粒度授: ```java import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ExternalApiController { @GetMapping("/api/external/data") @PreAuthorize("hasAuthority('READ_DATA')") // 检查客户端是否有 READ_DATA 限 public String getData() { return "Protected data for external client"; } } ``` 使用 `@PreAuthorize` 确保只有授客户端才能访问特定资源[^3]。 #### 4. **最佳实践和注意事项** - **安全增强**:使用 HTTPS 加密通信,避免 Token 泄露;定期轮换密钥。 - **性能考虑**:JWT 的无状态特性适合高并发,但 Token 验证会增加少量开销。 - **错误处理**:返回标准 HTTP 状态码,如 401(未认证)或 403(未授)。 - **测试**:使用 Postman 或单元测试验证认证流程。 引用[^4] 提到网关过滤器的使用,如果系统涉及 API 网关(如 Spring Cloud Gateway),可以在网关层统一处理,跳过某些路径,但这通常用于内部优化。对于纯外部访问,直接在 Spring Boot 应用层实现更安全可靠。 #### 5. **总结** 在 Spring Boot 中实现外部接口访问认证,核心是结合 Spring Security 的灵活机制,通过 JWT、OAuth2 或 API 密钥验证客户端身份。本文以 JWT 为例,详细展示了配置、过滤器和授注解的实现步骤,确保系统安全性和可靠性。实际部署时,建议结合具体需求选择方案,并参考 Spring 官方文档进行扩展。通过这种方式,您可以高效保护 API 免受未授访问[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值