You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中Grizzly+JAX-RS令牌认证出现401异常求助

问题排查:基于Grizzly的JAX-RS JWT认证异常

我在基于Grizzly的JAX-RS项目中实现HelloWorld程序的令牌认证,已完成服务器、客户端、资源类,以及JWT相关的TokenProvider和TokenFilter类,但运行客户端时抛出异常,已核对令牌生成与客户端传入的令牌一致,仍未解决问题。

服务器类代码

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

import java.io.IOException;
import java.net.URI;

public class HelloServer extends TokenFilter{
    public static final String BASE_URI = "http://localhost:2000/";

    public static HttpServer startServer() {
        final TokenFilter tokenFilter = new TokenFilter();

        final ResourceConfig rc = new ResourceConfig()
                                    .packages("com.example.assignment1")
                                    .register(tokenFilter);

        return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
    }

    public static void main(String[] args) throws IOException {
        final HttpServer server = startServer();
        System.out.printf("Jersey app started with endpoints available at "
                + "%s%nHit Ctrl-C to stop it...%n", BASE_URI);
        System.in.read();
        server.shutdown();
    }
}

客户端类代码

import java.util.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import static com.example.assignment1.HelloServer.BASE_URI;

public class HelloClient {
    private static final String TOKEN = "your-jwt-token-here";
    public static void main(String[] args) throws IOException {
        pingHelloWorld("akshata");
        System.out.println("Program closing!");
    }
    private static void pingHelloWorld(String word) throws IOException {
        URL url = new URL(BASE_URI + "hello-world?name=" + word);

        HttpURLConnection http = (HttpURLConnection)url.openConnection();
        http.setRequestProperty("Authorization", "Bearer " + TOKEN);

        Map<String, List<String>> headers = http.getHeaderFields();
        for (String key : headers.keySet()) {
            System.out.println(key + ": " + headers.get(key));
        }

        BufferedReader br = new BufferedReader(new InputStreamReader(http.getInputStream()));
        String response = br.readLine();
        System.out.printf("\n %s \n%n", response);
        http.disconnect();
    }
}

资源类代码

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;

@Path("/hello-world")
public class HelloResource {
    @GET
    @Produces("text/plain")
    @RolesAllowed("user")
    public String hello() {
        return "Hello, World!";
    }

    @GET
    @Path("/greet")
    @Produces("text/plain")
    @RolesAllowed("admin")
    public Response getHelloWorld(@QueryParam("name") String name) {
        String message = "Hello world " + name;
        return Response.status(200).entity(message).build();
    }
}

TokenProvider类代码

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.xml.bind.DatatypeConverter;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

public class TokenProvider {
    private static final String SECRET_KEY = "mySecretKey12345";

    public static String generateToken(String subject, String issuer, long ttlMillis) {
        if (ttlMillis <= 0) {
            throw new RuntimeException("Expiry time must be greater than zero : [" + ttlMillis + "] ");
        }
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        System.out.println(String.format("Generating token for subject: %s, issuer: %s, expiry: %d", subject, issuer, ttlMillis));

        JwtBuilder builder = Jwts.builder().setIssuedAt(now).setSubject(subject).setIssuer(issuer).signWith(signatureAlgorithm, signingKey);

        Date exp = null;
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        return builder.compact();
    }

    public static Claims validateToken(String token) {
        return Jwts.parser()
                .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))
                .parseClaimsJws(token).getBody();
    }
}

TokenFilter类代码

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;

import java.io.IOException;

public class TokenFilter implements ContainerRequestFilter {
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_PREFIX = "Bearer ";

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String authorizationHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER);
        if (authorizationHeader == null || !authorizationHeader.startsWith(AUTHORIZATION_PREFIX)) {
            requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
            return;
        }
        String token = authorizationHeader.substring(AUTHORIZATION_PREFIX.length());
        try {
            TokenProvider.validateToken(token);
        } catch (Exception e) {
            requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
        }
    }
}

核心排查点

  • 客户端令牌未替换:当前客户端TOKEN是占位符"your-jwt-token-here",必须替换为TokenProvider.generateToken()生成的有效JWT。可在服务器main方法中添加测试令牌生成代码,复制生成的字符串到客户端:

    String testToken = TokenProvider.generateToken("test-user", "hello-server", 3600000);
    System.out.println("测试令牌:" + testToken);
    
  • 密钥解析逻辑错误:DatatypeConverter.parseBase64Binary(SECRET_KEY)要求密钥是Base64编码字符串,但当前SECRET_KEY是明文,导致签名验证失败。两种修复方式:

    1. 将明文密钥转为Base64(mySecretKey12345转Base64为bXlTZWNyZXRLZXkxMjM0NQ==)后替换SECRET_KEY
    2. 直接用明文密钥构建签名密钥,修改TokenProvider:
      // 生成密钥部分
      byte[] apiKeySecretBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8);
      Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
      // 验证令牌部分
      public static Claims validateToken(String token) {
          return Jwts.parser()
                  .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                  .parseClaimsJws(token).getBody();
      }
      
  • @RolesAllowed未生效:TokenFilter仅验证令牌有效性,未提取角色并注入安全上下文,导致注解拦截请求。需修改TokenFilter添加安全上下文设置:

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        // 原有校验逻辑...
        try {
            Claims claims = TokenProvider.validateToken(token);
            String role = claims.get("role", String.class);
            requestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> claims.getSubject();
                }
                @Override
                public boolean isUserInRole(String roleName) {
                    return roleName.equals(role);
                }
                @Override
                public boolean isSecure() {
                    return requestContext.getUriInfo().getRequestUri().getScheme().equals("https");
                }
                @Override
                public String getAuthenticationScheme() {
                    return SecurityContext.BASIC_AUTH;
                }
            });
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常详情定位问题
            requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
        }
    }
    

    同时生成令牌时要添加角色声明:

    JwtBuilder builder = Jwts.builder()
            .setIssuedAt(now)
            .setSubject(subject)
            .setIssuer(issuer)
            .claim("role", "user") // 按需设置user/admin角色
            .signWith(signatureAlgorithm, signingKey);
    
  • 请求路径不匹配:客户端调用的路径对应资源类的无参数hello()方法,但传入了name参数,正确路径应为BASE_URI + "hello-world/greet?name=" + word,否则会因角色不匹配或路径错误返回401。

  • 缺失异常日志:在TokenFilter的catch块添加e.printStackTrace(),可查看具体验证失败原因(如签名无效、令牌过期等)。

内容的提问来源于stack exchange,提问作者ash

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 19:27:01