一:openfire登陆和退出报文源码解读:
openfire提供给开发者的接口是AuthProvider,通过实现该接口的
void authenticate(String var1, String var2) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException;
String getPassword(String var1) throws UserNotFoundException, UnsupportedOperationException;
void setPassword(String var1, String var2) throws UserNotFoundException, UnsupportedOperationException;
boolean supportsPasswordRetrieval();
boolean isScramSupported();
String getSalt(String var1) throws UnsupportedOperationException, UserNotFoundException;
int getIterations(String var1) throws UnsupportedOperationException, UserNotFoundException;
String getServerKey(String var1) throws UnsupportedOperationException, UserNotFoundException;
String getStoredKey(String var1) throws UnsupportedOperationException, UserNotFoundException;
openfire提供了DefaultAuthProvider,CrowdAuthProvider等实现方式。
以上是开发的接口。
除了上面的以外,我们需要知道openfire是如何进行验证的,在什么位置验证的,是否还有其它的接口给我们使用,比如监听器。
首先看我们通过调试寻找到相关的类和相关的代码。
in StanzaHandler.java
public void process(String stanza, XMPPPacketReader reader) throws Exception {
boolean initialStream = stanza.startsWith("<stream:stream") || stanza.startsWith("<flash:stream");
if (!sessionCreated || initialStream) {
//处理报文<stream:stream xmlns='jabber:client' to='localhost' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='milo@localhost' xml:lang='en'>
if (!initialStream) {
// Allow requests for flash socket policy files directly on the client listener port
if (stanza.startsWith("<policy-file-request/>")) {
String crossDomainText = FlashCrossDomainServlet.CROSS_DOMAIN_TEXT +
XMPPServer.getInstance().getConnectionManager().getClientListenerPort() +
FlashCrossDomainServlet.CROSS_DOMAIN_END_TEXT + '\0';
connection.deliverRawText(crossDomainText);
return;
}
else {
// Ignore <?xml version="1.0"?>
return;
}
}
// Found an stream:stream tag...
if (!sessionCreated) {
sessionCreated = true;
MXParser parser = reader.getXPPParser();
parser.setInput(new StringReader(stanza));
createSession(parser);
}
else if (startedTLS) {
//处理<stream:stream xmlns='jabber:client' to='localhost' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='zeng@localhost' xml:lang='en'>,使用tls协议
startedTLS = false;
tlsNegotiated();
}
else if (startedSASL && saslStatus == SASLAuthentication.Status.authenticated) {
startedSASL = false;
saslSuccessful();
}
else if (waitingCompressionACK) {
waitingCompressionACK = false;
compressionSuccessful();
}
return;
}
// Verify if end of stream was requested,这儿处理</stream:stream>的登出报文
if (stanza.equals("</stream:stream>")) {
if (session != null) {
session.getStreamManager().formalClose();
Log.debug( "Closing session as an end-of-stream was received: {}", session );
session.close();
}
return;
}
// Ignore <?xml version="1.0"?> stanzas sent by clients
if (stanza.startsWith("<?xml")) {
return;
}
// Create DOM object from received stanza
Element doc = reader.read(new StringReader(stanza)).getRootElement();
if (doc == null) {
// No document found.
return;
}
String tag = doc.getName();
if ("starttls".equals(tag)) {
//处理<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'></starttls>报文,约定对于账户密码使用TLS加密
// Negotiate TLS
if (negotiateTLS()) {
startedTLS = true;
}
else {
connection.close();
session = null;
}
}
else if ("auth".equals(tag)) {
//这儿处理<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>biwsbj16ZW5nLHI9XyJ4fW1PPVotNFIvb1htPWgubm5NYU85bnw5KUNiRCU=</auth>报文
// User is trying to authenticate using SASL
startedSASL = true;
// Process authentication stanza
saslStatus = SASLAuthentication.handle(session, doc);
} else if (startedSASL && "response".equals(tag) || "abort".equals(tag)) {
//这儿处理<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>Yz1iaXdzLHI9XyJ4fW1PPVotNFIvb1htPWgubm5NYU85bnw5KUNiRCU1N2VkMzA0ZC0yYjc1LTQ2NjAtYThlYi0wYWMwMmU3NWVmYTkscD1kNGxCcDFPbnBJbEI4cWRveE9Bbld4elZlMms9</response>
// User is responding to SASL challenge. Process response
saslStatus = SASLAuthentication.handle(session, doc);
}
else if ("compress".equals(tag)) {
// Client is trying to initiate compression
if (compressClient(doc)) {
// Compression was successful so open a new stream and offer
// resource binding and session establishment (to client sessions only)
waitingCompressionACK = true;
}
} else if (isStreamManagementStanza(doc)) {
session.getStreamManager().process( doc );
}
else {
process(doc);
}
}
相关执行流程StanzaHandler.process()->SASLAuthentication.handle()->ScramSha1SaslServer.getSalt()->AuthFactory.getSalt()->DefaultAuthProvider.getSalt()
登入的报文:
<stream:stream xmlns='jabber:client' to='localhost' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='milo@localhost' xml:lang='en'>
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'></starttls>
<stream:stream xmlns='jabber:client' to='localhost' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='milo@localhost' xml:lang='en'>
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>biwsbj1taWxvLHI9WVR4dlY9TW1ueUhQLSRWUjpQKUxuaSdFPyRWNEw6ejo=</auth>
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>Yz1iaXdzLHI9WVR4dlY9TW1ueUhQLSRWUjpQKUxuaSdFPyRWNEw6ejozMDU3MTliZi1mNDFmLTQ3NTctODQyZC02NWJmYWZhZDMxZjcscD1HMjJ4VmZiaGdZZWZLcTlqcUxpbU5EQjlGeTA9</response>
登出报文如下:
<presence id='6Y86M-86' type='unavailable'></presence></stream:stream>
现在来session相关的,这个是经常使用的,我们常常需要知道当前有那些客户端连接着就是通过session完成的,还有报文的发送和接受。
private static final Logger Log = LoggerFactory.getLogger(LocalClientSession.class);
private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
/**
* Keep the list of IP address that are allowed to connect to the server.
*
* If the list is empty then anyone is allowed to connect to the server, unless the IP is on the blacklist (which
* always takes precedence over the whitelist).
*
* Note: the values in this list can be hostnames, IP addresses or IP ranges (with wildcards).
*/
private static Set<String> allowedIPs = new HashSet<>();
private static Set<String> allowedAnonymIPs = new HashSet<>();
/**
* Similar to {@link #allowedIPs}, but used for blacklisting rather than whitelisting.
*/
private static Set<String> blockedIPs = new HashSet<>();
private boolean messageCarbonsEnabled;
/**
* The authentication token for this session.
*/
protected AuthToken authToken;
/**
* Flag indicating if this session has been initialized yet (upon first available transition).
*/
private boolean initialized;
/**
* Flag that indicates if the session was available ever.
*/
private boolean wasAvailable = false;
/**
* Flag indicating if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*/
private boolean offlineFloodStopped = false;
private Presence presence = null;
private int conflictCount = 0;
/**
* Privacy list that overrides the default privacy list. This list affects only this
* session and only for the duration of the session.
*/
private String activeList;
/**
* Default privacy list used for the session's user. This list is processed if there
* is no active list set for the session.
*/
private String defaultList;
sessionManager.java,实际执行创建的位置
/**
* Counter of user connections. A connection is counted just after it was created and not
* after the user became available. This counter only considers sessions local to this JVM.
* That means that when running inside of a cluster you will need to add up this counter
* for each cluster node.
*/
private final AtomicInteger connectionsCounter = new AtomicInteger(0);
/**
* Cache (unlimited, never expire) that holds information about client sessions (as soon as
* a resource has been bound). The cache is used by Remote sessions to avoid generating big
* number of remote calls.
* Key: full JID, Value: ClientSessionInfo
*/
private Cache<String, ClientSessionInfo> sessionInfoCache;
/**
* Cache (unlimited, never expire) that holds external component sessions.
* Key: component address, Value: nodeID
*/
private Cache<String, byte[]> componentSessionsCache;
/**
* Cache (unlimited, never expire) that holds sessions of connection managers. For each
* socket connection of the CM to the server there is going to be an entry in the cache.
* Key: full address of the CM that identifies the socket, Value: nodeID
*/
private Cache<String, byte[]> multiplexerSessionsCache;
/**
* Cache (unlimited, never expire) that holds incoming sessions of remote servers.
* Key: stream ID that identifies the socket/session, Value: nodeID
*/
private Cache<StreamID, byte[]> incomingServerSessionsCache;
/**
* Cache (unlimited, never expire) that holds list of incoming sessions
* originated from the same remote server (domain/subdomain). For instance, jabber.org
* may have 2 connections to the server running in jivesoftware.com (one socket to
* jivesoftware.com and the other socket to conference.jivesoftware.com).
* Key: remote hostname (domain/subdomain), Value: list of stream IDs that identify each socket.
*/
private Cache<String, List<StreamID>> hostnameSessionsCache;
/**
* Cache (unlimited, never expire) that holds domains, subdomains and virtual
* hostnames of the remote server that were validated with this server for each
* incoming server session.
* Key: stream ID, Value: Domains and subdomains of the remote server that were
* validated with this server.<p>
*
* This same information is stored in {@link LocalIncomingServerSession} but the
* reason for this duplication is that when running in a cluster other nodes
* will have access to this clustered cache even in the case of this node going
* down.
*/
private Cache<StreamID, Set<String>> validatedDomainsCache;
//这儿的监听器都是只针对监听connection关闭的。
private ClientSessionListener clientSessionListener = new ClientSessionListener();
private ComponentSessionListener componentSessionListener = new ComponentSessionListener();
private IncomingServerSessionListener incomingServerListener = new IncomingServerSessionListener();
private OutgoingServerSessionListener outgoingServerListener = new OutgoingServerSessionListener();
private ConnectionMultiplexerSessionListener multiplexerSessionListener = new ConnectionMultiplexerSessionListener();
/**
* Creates a new <tt>ClientSession</tt> with the specified streamID.
*
* @param conn the connection to create the session from.
* @param id the streamID to use for the new session.
* @param language The language to use for the new session.
* @return a newly created session.
*/
public LocalClientSession createClientSession(Connection conn, StreamID id, Locale language) {
if (serverName == null) {
throw new IllegalStateException("Server not initialized");
}
LocalClientSession session = new LocalClientSession(serverName, conn, id, language);
conn.init(session);
// Register to receive close notification on this session so we can
// remove and also send an unavailable presence if it wasn't
// sent before
//注册Connection的close监听器。
conn.registerCloseListener(clientSessionListener, session);
// Add to pre-authenticated sessions.
localSessionManager.getPreAuthenticatedSessions().put(session.getAddress().getResource(), session);
// Increment the counter of user sessions
connectionsCounter.incrementAndGet();
return session;
}
二、Connection和Session
- connection接口是和:
boolean validate();
void init( LocalSession session );
void reinit( LocalSession session );
String getHostAddress() throws UnknownHostException;
Certificate[] getLocalCertificates();
Certificate[] getPeerCertificates();
void setUsingSelfSignedCertificate( boolean isSelfSigned );
boolean isUsingSelfSignedCertificate();
@Override
void close();
void systemShutdown();
boolean isClosed();
boolean isSecure();
void registerCloseListener( ConnectionCloseListener listener, Object handbackMessage );
void removeCloseListener( ConnectionCloseListener listener );
void deliver( Packet packet ) throws UnauthorizedException;
void deliverRawText( String text );
boolean isFlashClient();
void setFlashClient( boolean flashClient );
int getMajorXMPPVersion();
int getMinorXMPPVersion();
void setXMPPVersion( int majorVersion, int minorVersion );
boolean isCompressed();
CompressionPolicy getCompressionPolicy();
void setCompressionPolicy(CompressionPolicy compressionPolicy);
TLSPolicy getTlsPolicy();
void setTlsPolicy(TLSPolicy tlsPolicy);
PacketDeliverer getPacketDeliverer();
@Deprecated
void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception;
void startTLS(boolean clientMode) throws Exception;
void addCompression();
void startCompression();
ConnectionConfiguration getConfiguration();
- Session的接口:
@Override
JID getAddress();
int getStatus();
StreamID getStreamID();
String getServerName();
Date getCreationDate();
Date getLastActiveDate();
long getNumClientPackets();
long getNumServerPackets();
void close();
boolean isClosed();
boolean isSecure();
Certificate[] getPeerCertificates();
String getHostAddress() throws UnknownHostException;
String getHostName() throws UnknownHostException;
@Override
void process( Packet packet );
void deliverRawText( String text );
boolean validate();
String getCipherSuiteName();
Locale getLanguage();
- Connection和Session的区别:
Connection是接口是对MINA的IoSession和LocalSession的一个包装。IoSession提供了诸如write,read等接口。也就是Connection是io位置的对象,它的一个实现类是NIOConnection,LocalSession是Session实现。
Session是Connection的升华,它的数据是建立在Connection中,它的不同之处是区别不同的Connection,也就是它有诸如JID getAddress()这样的方法,它保存了客户的身份信息。它的一个实现是LocalSession。
三、ConnectionCloseListener和SessionEventListener
- ConnectionCloseListener对应Connction
void onConnectionClose( Object handback );
- SessionEventListener对应的Session
void sessionCreated( Session session );
void sessionDestroyed( Session session );
void anonymousSessionCreated( Session session );
void anonymousSessionDestroyed( Session session );
void resourceBound( Session session );
总结:我们经常使用的就只有sessionManager这个类和扩展自己的用户验证规则AuthProvider。从sessionManager这个里面获取所有想要的LocalClientSession信息,其次有时候会自己扩展SessionEventListener。