1818import static reactor .core .scheduler .Schedulers .boundedElastic ;
1919
2020import io .netty .handler .codec .http .HttpHeaderNames ;
21+ import io .netty .handler .codec .http .HttpHeaders ;
2122import java .net .InetSocketAddress ;
2223import java .security .PrivilegedAction ;
2324import java .util .Base64 ;
4950 */
5051public final class SpnegoAuthProvider {
5152
53+ private static final String SPNEGO_HEADER = "Negotiate" ;
54+
5255 private final SpnegoAuthenticator authenticator ;
5356 private final GSSManager gssManager ;
57+ private final int unauthorizedStatusCode ;
58+
59+ private volatile String verifiedAuthHeader ;
5460
5561 /**
5662 * Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
5763 *
5864 * @param authenticator the authenticator to use for JAAS login
5965 * @param gssManager the GSSManager to use for SPNEGO token generation
6066 */
61- private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager ) {
67+ private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager , int unauthorizedStatusCode ) {
6268 this .authenticator = authenticator ;
6369 this .gssManager = gssManager ;
70+ this .unauthorizedStatusCode = unauthorizedStatusCode ;
6471 }
6572
6673 /**
6774 * Creates a new SPNEGO authentication provider using the default GSSManager instance.
6875 *
6976 * @param authenticator the authenticator to use for JAAS login
77+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
7078 * @return a new SPNEGO authentication provider
7179 */
72- public static SpnegoAuthProvider create (SpnegoAuthenticator authenticator ) {
73- return create (authenticator , GSSManager .getInstance ());
80+ public static SpnegoAuthProvider create (SpnegoAuthenticator authenticator , int unauthorizedStatusCode ) {
81+ return create (authenticator , GSSManager .getInstance (), unauthorizedStatusCode );
7482 }
7583
7684 /**
@@ -81,10 +89,11 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
8189 *
8290 * @param authenticator the authenticator to use for JAAS login
8391 * @param gssManager the GSSManager to use for SPNEGO token generation
92+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
8493 * @return a new SPNEGO authentication provider
8594 */
86- public static SpnegoAuthProvider create (SpnegoAuthenticator authenticator , GSSManager gssManager ) {
87- return new SpnegoAuthProvider (authenticator , gssManager );
95+ public static SpnegoAuthProvider create (SpnegoAuthenticator authenticator , GSSManager gssManager , int unauthorizedStatusCode ) {
96+ return new SpnegoAuthProvider (authenticator , gssManager , unauthorizedStatusCode );
8897 }
8998
9099 /**
@@ -100,24 +109,33 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
100109 * @throws RuntimeException if login or token generation fails
101110 */
102111 public Mono <Void > apply (HttpClientRequest request , InetSocketAddress address ) {
112+ String hostName = address .getHostName ();
113+ // 이미 토큰이 있고, 같은 호스트면 재사용
114+ if (verifiedAuthHeader != null ) {
115+ request .header (HttpHeaderNames .AUTHORIZATION , verifiedAuthHeader );
116+ return Mono .empty ();
117+ }
118+
103119 return Mono .fromCallable (() -> {
104120 try {
105121 return Subject .doAs (
106122 authenticator .login (),
107123 (PrivilegedAction <byte []>) () -> {
108124 try {
109- byte [] token = generateSpnegoToken (address .getHostName ());
110- String authHeader = "Negotiate " + Base64 .getEncoder ().encodeToString (token );
125+ byte [] token = generateSpnegoToken (hostName );
126+ String authHeader = SPNEGO_HEADER + " " + Base64 .getEncoder ().encodeToString (token );
127+
128+ verifiedAuthHeader = authHeader ;
111129 request .header (HttpHeaderNames .AUTHORIZATION , authHeader );
112130 return token ;
113131 }
114- catch (GSSException e ) {
132+ catch (GSSException e ) {
115133 throw new RuntimeException ("Failed to generate SPNEGO token" , e );
116134 }
117135 }
118136 );
119137 }
120- catch (LoginException e ) {
138+ catch (LoginException e ) {
121139 throw new RuntimeException ("Failed to login with SPNEGO" , e );
122140 }
123141 })
@@ -143,4 +161,36 @@ private byte[] generateSpnegoToken(String hostName) throws GSSException {
143161 GSSContext context = gssManager .createContext (serverName , spnegoOid , null , GSSContext .DEFAULT_LIFETIME );
144162 return context .initSecContext (new byte [0 ], 0 , 0 );
145163 }
164+
165+ /**
166+ * Invalidates the cached authentication token.
167+ * <p>
168+ * This method should be called when a response indicates that the current token
169+ * is no longer valid (typically after receiving an unauthorized status code).
170+ * The next request will generate a new authentication token.
171+ * </p>
172+ */
173+ public void invalidateCache () {
174+ this .verifiedAuthHeader = null ;
175+ }
176+
177+ /**
178+ * Checks if the response indicates an authentication failure that requires a new token.
179+ * <p>
180+ * This method checks both the status code and the WWW-Authenticate header to determine
181+ * if a new SPNEGO token needs to be generated.
182+ * </p>
183+ *
184+ * @param status the HTTP status code
185+ * @param headers the HTTP response headers
186+ * @return true if the response indicates an authentication failure
187+ */
188+ public boolean isUnauthorized (int status , HttpHeaders headers ) {
189+ if (status != unauthorizedStatusCode ) {
190+ return false ;
191+ }
192+
193+ String header = headers .get (HttpHeaderNames .WWW_AUTHENTICATE );
194+ return header != null && header .startsWith (SPNEGO_HEADER );
195+ }
146196}
0 commit comments