IMAP 认证机制
IMAP 协议支持多种认证方式,其中最常见的是:
- 两步验证(LOGIN):客户端先请求用户名,再请求密码。
- 一步验证(PLAIN):客户端一次性提交用户名和密码。
下面分别介绍这两种方式的报文流程及在服务端的实现。
一、两步验证(LOGIN)
报文流程
客户端发起 AUTHENTICATE LOGIN
,服务端依次发送用户名和密码的 Base64 提示:
> A002 AUTHENTICATE LOGIN
< + VXNlcm5hbWU6 (Base64 编码的 "Username:")
> amlldGlAdGlvLmNvbQ== (Base64 编码的 "jieti@tio.com")
< + UGFzc3dvcmQ6 (Base64 编码的 "Password:")
> MDAwMDAwMDA= (Base64 编码的 "00000000")
< A002 OK AUTHENTICATE completed.
二、一步验证(PLAIN)
报文流程
在 CAPABILITY
响应中添加 AUTH=PLAIN
,客户端就会使用 PLAIN 机制,一次性发送用户名和密码:
41 authenticate PLAIN
+ (服务端发起空挑战)
AGJvYgAwMDAwMDAwMA== (Base64 编码的 "\0bob\00000000")
三、服务端实现
在 ImapServerAioHandler
中,对两种机制统一在 AUTHENTICATE
命令下处理,然后根据机制分别进入不同的状态。
// ImapServerAioHandler 中的部分处理逻辑
if (session.getState() == ImapSessionContext.State.AUTH_WAIT_USERNAME
|| session.getState() == ImapSessionContext.State.AUTH_WAIT_PASSWORD) {
String reply = imapService.handleAuthData(session, line);
if (reply != null) {
Tio.bSend(ctx, new ImapPacket(reply));
}
return;
}
case "AUTHENTICATE":
reply = imapService.handleAuthenticate(session, tag, args);
1. 处理 AUTHENTICATE 命令
public String handleAuthenticate(ImapSessionContext session, String tag, String mech) {
StringBuilder sb = new StringBuilder();
if (!"LOGIN".equalsIgnoreCase(mech) && !"PLAIN".equalsIgnoreCase(mech)) {
sb.append(tag).append(" BAD Unsupported authentication mechanism").append("\r\n");
return sb.toString();
}
session.setCurrentCommandTag(tag);
if ("LOGIN".equalsIgnoreCase(mech)) {
// 两步验证:先请求用户名
session.setState(ImapSessionContext.State.AUTH_WAIT_USERNAME);
String chal = Base64Utils.encodeToString("Username:".getBytes(StandardCharsets.UTF_8));
sb.append("+ ").append(chal).append("\r\n");
} else {
// 一步验证:直接进入密码处理状态
session.setState(ImapSessionContext.State.AUTH_WAIT_PASSWORD);
sb.append("+ ").append("\r\n");
}
return sb.toString();
}
2. 处理客户端返回的认证数据
public String handleAuthData(ImapSessionContext session, String data) {
String tag = session.getCurrentCommandTag();
StringBuilder sb = new StringBuilder();
try {
String decoded = Base64Utils.decodeToString(data);
if (session.getState() == ImapSessionContext.State.AUTH_WAIT_USERNAME) {
// 两步验证——收到用户名,继续请求密码
session.setUsername(decoded);
session.setState(ImapSessionContext.State.AUTH_WAIT_PASSWORD);
String chal = Base64Utils.encodeToString("Password:".getBytes(StandardCharsets.UTF_8));
sb.append("+ ").append(chal).append("\r\n");
} else if (session.getState() == ImapSessionContext.State.AUTH_WAIT_PASSWORD) {
// 收到密码后,解析用户名和密码
String user, pass;
if (decoded.contains("\0")) {
// 一步验证:数据格式为 "\0user\0pass"
String[] parts = decoded.split("\0");
user = parts.length > 1 ? parts[1] : "";
pass = parts.length > 2 ? parts[2] : "";
} else {
// 两步验证:用户名已保存在 session 中
user = session.getUsername();
pass = decoded;
}
// 调用 userService 进行认证(内部实现无关)
Long userId = userService.authenticate(user, pass);
if (userId != null) {
session.setUsername(user);
session.setUserId(userId);
session.setState(ImapSessionContext.State.AUTHENTICATED);
sb.append(tag).append(" OK AUTHENTICATE completed.").append("\r\n");
} else {
session.setState(ImapSessionContext.State.NON_AUTHENTICATED);
sb.append(tag).append(" NO AUTHENTICATE failed: Authentication failed").append("\r\n");
}
session.setCurrentCommandTag(null);
}
} catch (IllegalArgumentException e) {
// Base64 解码失败
session.setState(ImapSessionContext.State.NON_AUTHENTICATED);
sb.append(tag).append(" BAD Invalid base64 data").append("\r\n");
session.setCurrentCommandTag(null);
}
return sb.toString();
}
四、小结
- 两步验证(LOGIN) 依次请求用户名和密码,更直观但多一次交互。
- 一步验证(PLAIN) 将用户名和密码合并在一次 Base64 数据中传输,需在
CAPABILITY
中声明AUTH=PLAIN
。 - 示例代码展示了如何在不同状态下解析客户端返回的数据,并调用
userService.authenticate
完成用户验证。
以上即为在 IMAP 服务端实现 LOGIN 与 PLAIN 两种认证方式的完整示例。