从安全的 websocket 连接中提取客户端 X509 证书

Extract client X509 certificate from a secured websocket connection

我想在 websocket 通信之上创建一个基于证书的身份验证。 所以我创建了一个 websocket serverEndpoint,并在 jetty 的帮助下为客户端身份验证设置了 SSL,如下所示:

Server server = new Server();

//Create SSL ContextFactory with appropriate attributes
SslContextFactory sslContextFactory = new SslContextFactory();
    //Set up keystore path, truststore path, passwords, etc
    ...
sslContextFactory.setNeedClientAuth(true);


//Create the connector
ServerConnector localhostConnector = new ServerConnector(server, sslContextFactory);
localhostConnector.setHost(...);
localhostConnector.setPort(...);
server.addConnector(localhostConnector);

//Create ContextHandler
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/example");
server.setHandler(context);

// Initialize the JSR-356 layer and add custom Endpoints
ServerContainer container = WebSocketServerContainerInitializer.configureContext(context);
container.addEndpoint(Endpoint1.class);   //Annotated class
container.addEndpoint(Endpoint2.class);   

SSL 配置似乎是正确的,因为我可以使用我编写的 SSL 客户端连接到不同的端点(错误的证书会导致连接终止)。

现在,我想提取客户端证书中包含的信息。我看到我可以从 SSLSession 获取证书,但是我在端点中可以访问的唯一会话是 "normal" Session:

@OnOpen
@Override
public void open(final Session session, final  EndpointConfig config)

有没有办法以某种方式存储证书或包含的信息并将其传递给端点?

感谢您的帮助:)

我找到了一个让客户端注册为会话的 UserPrincipal 的解决方案,可通过 session.getUserPrincipal() 访问。

UserPricipal 是 "the authenticated user for the session"。然后,您需要向 ServletContextHandler 添加身份验证服务,如下所示:

//Create SSL ContextFactory with appropriate attributes
...

//Create the connector
...

//Create ContextHandler
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    context.setContextPath("/example");

//Add security contraint to the context => authentication

ConstraintSecurityHandler security = new ConstraintSecurityHandler();

Constraint constraint = new Constraint();
constraint.setName("auth");
constraint.setAuthenticate(true);
constraint.setRoles(new String[]{"user"});

Set<String> knownRoles = new HashSet<String>();
knownRoles.add("user");

ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/*");
mapping.setConstraint(constraint);

security.setConstraintMappings(Collections.singletonList(mapping), knownRoles);
security.setAuthMethod("CLIENT-CERT");

LoginService loginService = new HashLoginService();
security.setLoginService(loginService);
security.setAuthenticator(new ClientCertAuthenticator());

context.setSecurityHandler(security);

这样,当客户端连接到 websocket 端点时,安全处理程序确保客户端必须经过身份验证。据我了解,ClientCertAuthenticator 将检查客户端请求以提取信息(证书的 DN),然后将其传递给 LoginService,在其中对客户端进行身份验证并设置会话的 UserPricipal。

这里的问题是您必须有一个可用的登录服务(例如,HashLoginService 是一个使用密码和用户名的内存中登录服务,JDBCLoginService 使用数据库)。对于那些像我一样只想从证书中提取所需信息并随后使用这些信息进行身份验证的人,您可以提供自己的 LoginService 接口实现。

这是我所做的:

在定义安全处理程序期间:

 LoginService loginService = new CustomLoginService();
 loginService.setIdentityService(new DefaultIdentityService());
 security.setLoginService(loginService);

自定义登录服务Class

public class CustomLoginService implements LoginService {

IdentityService identityService = null;

@Override
public String getName() {
    return "";
}

@Override
public UserIdentity login(String username, Object credentials) {
    //you need to return a UserIdentity, which takes as argument:
    // 1. A Subjet, containing a set of principals, a set of private credentials and a set of public ones (type Object)
    // 2. A Principal of this Subject
    // 3. A set of roles (String)

    LdapPrincipal principal = null;

    try {
        principal = new LdapPrincipal(username);
        //you need to have a Principal. I chose LDAP because it is specifically intended for user identified with a DN.

    } catch (InvalidNameException e) {
        e.printStackTrace();
    }

    String[] roles = new String[]{"user"};
    return new DefaultUserIdentity(
             new Subject(false, 
                 new HashSet<LdapPrincipal>(Arrays.asList(new LdapPrincipal[]{principal}) ), 
                 new HashSet<Object>(Arrays.asList(new Object[]{credentials})), 
                 new HashSet<Object>(Arrays.asList(new Object[]{credentials}))), 
             principal, 
             roles);
}

@Override
public boolean validate(UserIdentity user) {

    return false;
}

@Override
public IdentityService getIdentityService() {
    return identityService;
}

@Override
public void setIdentityService(IdentityService service) {
    identityService = service;
}

@Override
public void logout(UserIdentity user) {

}

就是这样:)