我编写了一些代码,用于验证客户端在我的服务器上的Kerberos票证。我还为我的类编写了单元测试。单元测试是通过模拟调用GSS库类来编写的。但这并不足以使我充满信心,因为实际的GSS调用被模拟了。
根据我的研究,为了验证客户端的令牌,我需要使用我与KDC共享的密钥对其进行解密,我可以从keytab文件中获取该密钥。因此,为了执行验证,我需要两个东西(请纠正我):
1. 客户端的令牌 2. 服务器上的keytab文件
如果我在类路径中有这些文件,那么我是否可以执行令牌的实际验证而不需要任何模拟调用?这样做是否存在技术挑战?如果有,它们是什么?
更新1: 看起来我还需要设置一些系统属性,以便GSS库选择正确的领域、KDC等。因此,我们需要三件事:
1. Kerberos票证 2. keytab文件 3. 对应于keytab文件和票证的系统属性。
有了这个,似乎我能够得到一个端到端的测试,具有验证功能,但是只有5分钟。:)
情况是这样的,如果我拿起由KDC新生成的Kerberos令牌并将其放入我的测试中,则测试将成功运行,但在5分钟后开始失败,显示“时钟偏差太大”的异常。我更改了KDC上的Kerberos策略以生成一个永不过期的票证,但错误仍然存在。这里的好消息是,现在我有了一个概念验证,证明了这种方法的可行性。
问题归结为如何解决“时钟偏差太大”的错误。
更新2: 通过在krb.conf文件中指定它来修改时钟偏差值。那是另一个我需要设置的系统属性。有了这个,测试现在可以端到端地工作了。现在正在编写答案。
时钟偏差错误的堆栈跟踪:
根据我的研究,为了验证客户端的令牌,我需要使用我与KDC共享的密钥对其进行解密,我可以从keytab文件中获取该密钥。因此,为了执行验证,我需要两个东西(请纠正我):
1. 客户端的令牌 2. 服务器上的keytab文件
如果我在类路径中有这些文件,那么我是否可以执行令牌的实际验证而不需要任何模拟调用?这样做是否存在技术挑战?如果有,它们是什么?
更新1: 看起来我还需要设置一些系统属性,以便GSS库选择正确的领域、KDC等。因此,我们需要三件事:
1. Kerberos票证 2. keytab文件 3. 对应于keytab文件和票证的系统属性。
有了这个,似乎我能够得到一个端到端的测试,具有验证功能,但是只有5分钟。:)
情况是这样的,如果我拿起由KDC新生成的Kerberos令牌并将其放入我的测试中,则测试将成功运行,但在5分钟后开始失败,显示“时钟偏差太大”的异常。我更改了KDC上的Kerberos策略以生成一个永不过期的票证,但错误仍然存在。这里的好消息是,现在我有了一个概念验证,证明了这种方法的可行性。
问题归结为如何解决“时钟偏差太大”的错误。
更新2: 通过在krb.conf文件中指定它来修改时钟偏差值。那是另一个我需要设置的系统属性。有了这个,测试现在可以端到端地工作了。现在正在编写答案。
时钟偏差错误的堆栈跟踪:
Caused by: java.security.PrivilegedActionException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:422)
at com.example.vidm.eks.request.KerberosTokenValidator.getPrincipalUserName(KerberosTokenValidator.java:91)
at com.example.vidm.eks.request.KerberosTokenValidator.lambda$validateToken$0(KerberosTokenValidator.java:80)
... 7 more
Caused by: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:856)
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
at sun.security.jgss.spnego.SpNegoContext.GSS_acceptSecContext(SpNegoContext.java:906)
at sun.security.jgss.spnego.SpNegoContext.acceptSecContext(SpNegoContext.java:556)
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:47)
at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:22)
... 11 more
Caused by: KrbException: Clock skew too great (37)
at sun.security.krb5.KrbApReq.authenticate(KrbApReq.java:302)
at sun.security.krb5.KrbApReq.<init>(KrbApReq.java:149)
at sun.security.jgss.krb5.InitSecContextToken.<init>(InitSecContextToken.java:108)
at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:829)
... 19 more
我的功能测试代码:
public class KerberosTokenValidatorTest extends AbstractUnitTestBase {
public static final String NO_PRINCIPAL = null;
private String kerberosTicket;
public static final String USERNAME = "username";
private static final String REALM = "EXAMPLE.COM";
private static final String PRINCIPAL = USERNAME + "@" + REALM;
@BeforeClass
public void beforeClass(){
System.setProperty("java.security.krb5.kdc", "host/hw-99402.example.com");
System.setProperty("java.security.krb5.realm", "EXAMPLE.COM");
System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
String confFile = String.format("/tmp/%s", RandomStringUtils.random(10));
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("testkrb.conf")) {
Files.copy(is, Paths.get(confFile));
} catch (IOException e) {
// An error occurred copying the resource
}
System.setProperty("java.security.krb5.conf", confFile);
}
@Test
public void myTest() throws IOException, GSSException, ExecutionException, InterruptedException {
KerberosTokenValidator kerberosTokenValidator = new KerberosTokenValidator();
String kticket = FileSystemUtils.loadClasspathResourceAsString("kerberosticket");
kerberosTokenValidator.validateToken(kticket, "hw-99402.example.com", "userPrincipalName").get();
}
}
我的验证代码:
private String getPrincipalUserName(String token1, String serverName) throws LoginException, PrivilegedActionException {
javax.security.auth.Subject serviceSubject = getServiceSubject(serverName);
byte[] token = base64Decoder.decode(token1);
KerberosTicketValidation ticketValidation = javax.security.auth.Subject.doAs(serviceSubject, new KerberosValidateAction(token));
String kdcPrincipal = ticketValidation.getUsername();
if (StringUtils.isBlank(kdcPrincipal)) {
throw new LoginException("KDC principal is blank after ticket validation");
}
return kdcPrincipal;
}
private javax.security.auth.Subject getServiceSubject(String serverName) throws LoginException {
String servicePrincipal = SERVICE_PRINCIPAL_SERVICE + "/" + serverName;
final Set<Principal> princ = new HashSet<>(1);
princ.add(new KerberosPrincipal(servicePrincipal));
javax.security.auth.Subject sub = new javax.security.auth.Subject(false, princ, Collections.emptySet(), Collections.emptySet());
KerberosConfig kerberosConfig = new KerberosConfig(KEYTAB_PATH, servicePrincipal);
LoginContext lc = new LoginContext("", sub, null, kerberosConfig);
lc.login();
return lc.getSubject();
}
我的单元测试:
@BeforeMethod
public void setup() throws Exception {
reset(mockGSSContext, mockGSSManager, mockGSSName);
mockGSSManager();
}
@InjectMocks
private KerberosTokenValidator kerberosTokenValidator;
@Mock protected GSSManager mockGSSManager;
@Mock protected GSSContext mockGSSContext;
@Mock protected GSSName mockGSSName;
@Test
public void canValidateKerberosToken() throws Throwable {
when(mockGSSName.toString()).thenReturn(PRINCIPAL);
Subject subject = blockAndThrow(kerberosTokenValidator.validateToken(kerberosTicket, "hw-99402.vidmlabs.com", "sAMAccountName"));
Assert.assertEquals(subject.getNameId(), USERNAME);
}
private void mockGSSManager() throws Exception {
when(mockGSSManager.createContext((GSSCredential) null)).thenReturn(mockGSSContext);
when(mockGSSContext.isEstablished()).thenReturn(true);
when(mockGSSContext.acceptSecContext(any(byte[].class), anyInt(), anyInt())).thenReturn(null);
when(mockGSSContext.getSrcName()).thenReturn(mockGSSName);
KerberosValidateAction.setGssManager(mockGSSManager);
}
KerberosValidateAction :
public class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
private static GSSManager gssManager = GSSManager.getInstance();
private byte[] kerberosTicket;
private GSSCredential serviceCredentials;
public KerberosValidateAction(byte[] kerberosTicket) {
this(kerberosTicket, null);
}
public KerberosValidateAction(byte[] kerberosTicket, GSSCredential serviceCredentials) {
this.kerberosTicket = kerberosTicket;
this.serviceCredentials = serviceCredentials;
}
@VisibleForTesting
public static void setGssManager(GSSManager manager) {
gssManager = manager;
}
@Override
public KerberosTicketValidation run() throws Exception {
GSSName gssName = null;
GSSContext context = gssManager.createContext(serviceCredentials);
byte[] token = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
if (!context.isEstablished()) {
throw new ContinueNeededException(token);
}
gssName = context.getSrcName();
if (gssName == null) {
throw new AuthenticationException("GSSContext name of the context initiator is null");
}
context.dispose();
return new KerberosTicketValidation(gssName.toString());
}
}
krb5.conf文件:
[libdefaults]
clockskew = 999999999