首先,我使用以下示例代码生成Kerberos协商令牌:
package com.onenew.http.kerberos.test;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.codec.binary.Base64;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
public class App {
private static final String loginContextName = "xyz";
private static final String userName = "username@EXAMPLE.COM";
private static final String targetServerSpn = "server/EXAMPLE1.COM@EXAMPLE.COM";
public static void main(String[] args) {
try {
System.setProperty("java.security.auth.login.config", "/var/jaas.conf");
Oid krb5MechOid = new Oid("1.2.840.113554.1.2.2");
Oid krb5PrincipalNameOid = new Oid("1.2.840.113554.1.2.2.1");
GSSManager manager = GSSManager.getInstance();
LoginContext lc = new LoginContext(loginContextName);
lc.login();
Subject subject = lc.getSubject();
GSSCredential clientGssCreds = (GSSCredential) Subject.doAs(subject,
new CreateClientGSSCredentialAction(krb5MechOid, krb5PrincipalNameOid, manager));
// create target server SPN
GSSName gssServerName = manager.createName(targetServerSpn, krb5PrincipalNameOid);
GSSContext clientContext = manager.createContext(gssServerName, krb5MechOid,
clientGssCreds, GSSContext.DEFAULT_LIFETIME);
// optional enable GSS credential delegation
clientContext.requestCredDeleg(true);
byte[] spnegoToken = new byte[0];
// create a SPNEGO token for the target server
spnegoToken = clientContext.initSecContext(spnegoToken, 0, spnegoToken.length);
String token = Base64.encodeBase64String(spnegoToken);
System.out.println("Base64 Token:"+token);
clientContext.dispose();
}catch (GSSException e) {
System.out.println("GSSException");
System.out.println(e.getMessage());
} catch (PrivilegedActionException e) {
System.out.println("PrivilegedActionException");
System.out.println(e.getMessage());
} catch (LoginException e) {
System.out.println("LoginException");
System.out.println(e.getMessage());
}
}
private static final class CreateClientGSSCredentialAction implements PrivilegedExceptionAction<GSSCredential> {
private Oid krb5MechOid;
private Oid krb5PrincipalNameOid;
private GSSManager manager;
private CreateClientGSSCredentialAction(Oid krb5MechOid, Oid krb5PrincipalNameOid, GSSManager manager) {
this.krb5MechOid = krb5MechOid;
this.krb5PrincipalNameOid = krb5PrincipalNameOid;
this.manager = manager;
}
public GSSCredential run() throws GSSException, Exception {
try {
GSSName gssClientName = manager.createName(userName, krb5PrincipalNameOid);
GSSCredential gssClientCred = manager.createCredential(gssClientName,
GSSCredential.DEFAULT_LIFETIME, krb5MechOid, GSSCredential.INITIATE_ONLY);
return gssClientCred;
} catch (GSSException e) {
System.out.println("run method GSSException");
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.println("run method Exception");
System.out.println(e.getMessage());
}
return null;
}
}
}
然后我使用生成的令牌请求由Jetty和Jersey创建的REST服务,在REST服务上有一个从ContainerRequestFilter实现的KerberosFilter,下面是代码:
package com.onenew.rest.security.filter;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Set;
import javax.ws.rs.core.Response;
import io.confluent.kafka.schemaregistry.rest.SchemaRegistryConfig;
import io.confluent.kafka.schemaregistry.storage.SchemaRegistry;
import io.confluent.rest.RestConfigException;
import org.apache.cxf.common.security.SimplePrincipal;
import org.apache.cxf.common.security.SimpleSecurityContext;
import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class KafkaKerberosFilter implements ContainerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(KafkaKerberosFilter.class);
private static String REALM="EXAMPLE.COM";
private static final String AUTHENTICATION_SCHEME = "Negotiate";
private static final String KERBEROS_OID = "1.2.840.113554.1.2.2";
private static final String KERBEROS_PRINCIPAL_OID="1.2.840.113554.1.2.2.1";
private String loginContextName = "xyzService";
private String servicePrincipalName;
public KafkaKerberosFilter() {
}
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if(authorizationHeader==null||authorizationHeader.length()==0){
String errorMsg="invalid authorization header";
log.error(errorMsg);
abortWithUnauthorized(requestContext,errorMsg);
return;
}
String[] authPair = authorizationHeader.split(" ");
if (authPair.length != 2 || !AUTHENTICATION_SCHEME.equalsIgnoreCase(authPair[0])) {
String errorMsg="Negotiate Authorization scheme is expected";
log.error(errorMsg);
abortWithUnauthorized(requestContext,errorMsg);
return;
}
log.debug("Base64 Token:"+authPair[1]);
byte[] serviceTicket;
try{
serviceTicket = Base64Utility.decode(authPair[1]);
log.debug("Decoded Token:"+serviceTicket.toString());
}catch(Base64Exception e){
String errorMsg="can't decode kerberos ticket";
log.error(errorMsg+" "+authPair[1]);
abortWithUnauthorized(requestContext,errorMsg);
return;
}
try{
Subject serviceSubject = loginAndGetSubject();
GSSContext gssContext = createGSSContext(requestContext);
Subject.doAs(serviceSubject, new ValidateServiceTicketAction(gssContext, serviceTicket));
if (!gssContext.getCredDelegState()) {
gssContext.dispose();
gssContext = null;
}
}catch(LoginException e){
String errorMsg="LoginContext can not be initialized";
log.error(errorMsg+" "+loginContextName);
abortWithUnauthorized(requestContext,errorMsg);
return;
}catch (GSSException e) {
String errorMsg="GSS API exception";
log.error(errorMsg+" "+e.getMessage());
abortWithUnauthorized(requestContext,errorMsg);
return;
} catch (PrivilegedActionException e) {
String errorMsg="PrivilegedActionException";
log.error(errorMsg+" "+e.getMessage());
abortWithUnauthorized(requestContext,errorMsg);
return;
}
}
private void abortWithUnauthorized(ContainerRequestContext requestContext,String error) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\",\nerror_description=\""+error+"\"").build());
}
protected Subject loginAndGetSubject() throws LoginException {
// The login without a callback can work if
// - Kerberos keytabs are used with a principal name set in the JAAS config
// - Kerberos is integrated into the OS logon process
// meaning that a process which runs this code has the
// user identity
LoginContext lc = new LoginContext(loginContextName);
lc.login();
Subject subject = lc.getSubject();
return subject;
}
protected GSSContext createGSSContext(ContainerRequestContext requestContext) throws GSSException {
Oid principalOid = new Oid(KERBEROS_PRINCIPAL_OID);
Oid oid = new Oid(KERBEROS_OID);
GSSManager gssManager = GSSManager.getInstance();
String spn = getCompleteServicePrincipalName(requestContext);
GSSName gssService = gssManager.createName(spn,principalOid);
return gssManager.createContext(gssService,
oid, null, GSSContext.DEFAULT_LIFETIME);
}
protected String getCompleteServicePrincipalName(ContainerRequestContext requestContext) {
String name = servicePrincipalName == null
? "server/" + requestContext.getUriInfo().getBaseUri().getHost() : servicePrincipalName;
name += "@" + REALM;
log.debug("Service Principal Name:"+name);
return name;
}
public void setServicePrincipalName(String servicePrincipalName) {
this.servicePrincipalName = servicePrincipalName;
}
private static final class ValidateServiceTicketAction implements PrivilegedExceptionAction<byte[]> {
private final GSSContext context;
private final byte[] token;
private ValidateServiceTicketAction(GSSContext context, byte[] token) {
this.context = context;
this.token = token;
}
public byte[] run() throws GSSException {
try{
return context.acceptSecContext(token, 0, token.length);
}catch(GSSException e){
log.error("PrivilegedExceptionAction run error:"+e.getMessage());
throw e;
}
}
}
}
带有生成令牌的第一个请求是成功。但是以下具有相同令牌的请求全部失败。 ValidateServiceTicketAction类的方法运行抛出GSSException,它表示“在GSS-API级别未指定失败(机制级别:请求是重播(34))”。我用谷歌搜索,找不到解决方案,大多数答案都说时间必须同步,但是我已经在客户端,REST服务和KDC上一直保持同步。