18
18
import static reactor .core .scheduler .Schedulers .boundedElastic ;
19
19
20
20
import io .netty .handler .codec .http .HttpHeaderNames ;
21
+ import io .netty .handler .codec .http .HttpHeaders ;
21
22
import java .net .InetSocketAddress ;
22
23
import java .security .PrivilegedAction ;
23
24
import java .util .Base64 ;
49
50
*/
50
51
public final class SpnegoAuthProvider {
51
52
53
+ private static final String SPNEGO_HEADER = "Negotiate" ;
54
+
52
55
private final SpnegoAuthenticator authenticator ;
53
56
private final GSSManager gssManager ;
57
+ private final int unauthorizedStatusCode ;
58
+
59
+ private volatile String verifiedAuthHeader ;
54
60
55
61
/**
56
62
* Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
57
63
*
58
64
* @param authenticator the authenticator to use for JAAS login
59
65
* @param gssManager the GSSManager to use for SPNEGO token generation
60
66
*/
61
- private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager ) {
67
+ private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager , int unauthorizedStatusCode ) {
62
68
this .authenticator = authenticator ;
63
69
this .gssManager = gssManager ;
70
+ this .unauthorizedStatusCode = unauthorizedStatusCode ;
64
71
}
65
72
66
73
/**
67
74
* Creates a new SPNEGO authentication provider using the default GSSManager instance.
68
75
*
69
76
* @param authenticator the authenticator to use for JAAS login
77
+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
70
78
* @return a new SPNEGO authentication provider
71
79
*/
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 );
74
82
}
75
83
76
84
/**
@@ -81,10 +89,11 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
81
89
*
82
90
* @param authenticator the authenticator to use for JAAS login
83
91
* @param gssManager the GSSManager to use for SPNEGO token generation
92
+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
84
93
* @return a new SPNEGO authentication provider
85
94
*/
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 );
88
97
}
89
98
90
99
/**
@@ -100,24 +109,33 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
100
109
* @throws RuntimeException if login or token generation fails
101
110
*/
102
111
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
+
103
119
return Mono .fromCallable (() -> {
104
120
try {
105
121
return Subject .doAs (
106
122
authenticator .login (),
107
123
(PrivilegedAction <byte []>) () -> {
108
124
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 ;
111
129
request .header (HttpHeaderNames .AUTHORIZATION , authHeader );
112
130
return token ;
113
131
}
114
- catch (GSSException e ) {
132
+ catch (GSSException e ) {
115
133
throw new RuntimeException ("Failed to generate SPNEGO token" , e );
116
134
}
117
135
}
118
136
);
119
137
}
120
- catch (LoginException e ) {
138
+ catch (LoginException e ) {
121
139
throw new RuntimeException ("Failed to login with SPNEGO" , e );
122
140
}
123
141
})
@@ -143,4 +161,36 @@ private byte[] generateSpnegoToken(String hostName) throws GSSException {
143
161
GSSContext context = gssManager .createContext (serverName , spnegoOid , null , GSSContext .DEFAULT_LIFETIME );
144
162
return context .initSecContext (new byte [0 ], 0 , 0 );
145
163
}
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
+ }
146
196
}
0 commit comments