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是明文,导致签名验证失败。两种修复方式:- 将明文密钥转为Base64(
mySecretKey12345转Base64为bXlTZWNyZXRLZXkxMjM0NQ==)后替换SECRET_KEY - 直接用明文密钥构建签名密钥,修改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(); }
- 将明文密钥转为Base64(
@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
相关产品推荐
相关产品推荐

