在日常生活中,我們?cè)谝粋€(gè)網(wǎng)站中注冊(cè)一個(gè)賬戶(hù)時(shí),往往在提交個(gè)人信息后,網(wǎng)站還要我們通過(guò)手機(jī)或郵件來(lái)驗(yàn)證,郵件的話(huà)大概會(huì)是下面這個(gè)樣子的:

用戶(hù)通過(guò)點(diǎn)擊鏈接從而完成注冊(cè),然后才能登錄。
也許你會(huì)想,為什么要這么麻煩直接提交注冊(cè)不就行了嗎?這其中很大一部分原因是為了防止惡意注冊(cè)。接下來(lái)讓我們一起來(lái)使用最簡(jiǎn)單的JSP+Servlet的方式來(lái)完成一個(gè)通過(guò)郵箱驗(yàn)證注冊(cè)的小案例吧。
準(zhǔn)備工作
前提知識(shí)
動(dòng)手實(shí)踐之前,你最好對(duì)以下知識(shí)有所了解:
- JSP和Servlet
- Maven
- MySQL
- c3p0
- SMTP協(xié)議和POP3協(xié)議
如果對(duì)郵件收發(fā)過(guò)程完全不了解的話(huà),可以花三分鐘的時(shí)間到慕課網(wǎng)了解一下,講得算是非常清楚了,這里就不贅述了。放張圖回憶一下:

郵箱準(zhǔn)備
在了解的上述內(nèi)容之后,要實(shí)現(xiàn)這個(gè)案例,首先我們還得有兩個(gè)郵箱賬號(hào),一個(gè)用來(lái)發(fā)送郵件,一個(gè)用來(lái)接收郵件。本案例使用QQ郵箱向163郵箱發(fā)送激活郵件,因此需要登錄QQ郵箱,在設(shè)置->賬戶(hù)面板中開(kāi)啟POP3/SMTP服務(wù),以允許我們通過(guò)第三方客戶(hù)端發(fā)送郵件:

還要注意的是,登錄以下服務(wù): POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服務(wù)時(shí),需要用到授權(quán)碼而不是QQ密碼,授權(quán)碼是用于登錄第三方郵件客戶(hù)端的專(zhuān)用密碼。因此我們需要獲得授權(quán)碼,以在后面的程序中使用。
好了,到此準(zhǔn)備工作就差不多了,下面開(kāi)始動(dòng)手吧。
實(shí)現(xiàn)注冊(cè)Demo
創(chuàng)建Maven工程
本次案例基于Maven,因此你要先創(chuàng)建一個(gè)Maven的Web工程,并引入相關(guān)依賴(lài):
<dependencies>
<!-- JavaEE依賴(lài) -->
<dependency>
<groupId>javaee</groupId>
<artifactId>javaee-api</artifactId>
<version>5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- mysql驅(qū)動(dòng)依賴(lài) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<!-- c3p0依賴(lài) -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- JavaMail相關(guān)依賴(lài) -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
創(chuàng)建數(shù)據(jù)庫(kù)表
接下來(lái)使用MySQL創(chuàng)建一張簡(jiǎn)單的用戶(hù)表:
create table `user`(
id int(11) primary key auto_increment comment '用戶(hù)id',
username varchar(255) not null comment '用戶(hù)名',
email varchar(255) not null comment '用戶(hù)郵箱',
password varchar(255) not null comment '用戶(hù)密碼',
state int(1) not null default 0 comment '用戶(hù)激活狀態(tài):0表示未激活,1表示激活',
code varchar(255) not null comment '激活碼'
)engine=InnoDB default charset=utf8;
其中要注意的地方是state字段(用來(lái)判斷用戶(hù)賬號(hào)是否激活)和code字段(激活碼)。
創(chuàng)建注冊(cè)頁(yè)面
使用JSP創(chuàng)建一個(gè)最簡(jiǎn)單的注冊(cè)頁(yè)面(請(qǐng)自行忽略界面):

嗯,果然夠簡(jiǎn)單。
主要的業(yè)務(wù)邏輯
先想一下,我們的整個(gè)流程應(yīng)該是這樣的:
- 用戶(hù)填寫(xiě)相關(guān)信息,點(diǎn)擊注冊(cè)按鈕
- 系統(tǒng)先將用戶(hù)記錄保存到數(shù)據(jù)庫(kù)中,其中用戶(hù)狀態(tài)為未激活
- 系統(tǒng)發(fā)送一封郵件并通知用戶(hù)去驗(yàn)證
- 用戶(hù)登錄郵箱并點(diǎn)擊激活鏈接
- 系統(tǒng)將用戶(hù)狀態(tài)更改為已激活并通知用戶(hù)注冊(cè)成功
搞清楚了整個(gè)流程,實(shí)現(xiàn)起來(lái)應(yīng)該就不難了。下圖是我建立的包結(jié)構(gòu):

ps:完整代碼請(qǐng)見(jiàn)后文鏈接,這里只討論主要的思路
首先是,用戶(hù)提交注冊(cè)信息后,相應(yīng)的servlet會(huì)將相關(guān)信息傳給service層去處理,在service中需要做的就是講記錄保存到數(shù)據(jù)庫(kù)中(調(diào)用dao層),然后再給用戶(hù)發(fā)送一封郵件,UserServiceImpl相關(guān)代碼如下:
public boolean doRegister(String userName, String password, String email) {
// 這里可以驗(yàn)證各字段是否為空
//利用正則表達(dá)式(可改進(jìn))驗(yàn)證郵箱是否符合郵箱的格式
if(!email.matches("^\\w+@(\\w+\\.)+\\w+$")){
return false;
}
//生成激活碼
String code=CodeUtil.generateUniqueCode();
User user=new User(userName,email,password,0,code);
//將用戶(hù)保存到數(shù)據(jù)庫(kù)
UserDao userDao=new UserDaoImpl();
//保存成功則通過(guò)線(xiàn)程的方式給用戶(hù)發(fā)送一封郵件
if(userDao.save(user)>0){
new Thread(new MailUtil(email, code)).start();;
return true;
}
return false;
}
需要注意的是,應(yīng)該新建一個(gè)線(xiàn)程去執(zhí)行發(fā)送郵件的任務(wù),不然被罵估計(jì)是免不了了。
數(shù)據(jù)庫(kù)的操作比較簡(jiǎn)單,此處就不貼出來(lái)了,無(wú)非是將用戶(hù)記錄插到數(shù)據(jù)庫(kù)中。值得一提的是,此處使用c3p0來(lái)作為數(shù)據(jù)源來(lái)替代DriverManager,在頻繁獲取釋放數(shù)據(jù)庫(kù)連接時(shí)效率會(huì)大大提高,c3p0最簡(jiǎn)單的配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<named-config name="mysql">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://127.0.0.1:3306/test1?useSSL=false</property>
<property name="user">root</property>
<property name="password">123456</property>
<!-- 初始化時(shí)一個(gè)連接池嘗試獲得的連接數(shù)量,默認(rèn)是3,大小應(yīng)該在maxPoolSize和minPoolSize之間 -->
<property name="initialPoolSize">5</property>
<!-- 一個(gè)連接最大空閑時(shí)間(單位是秒),0意味著連接不會(huì)過(guò)時(shí) -->
<property name="maxIdleTime">30</property>
<!-- 任何指定時(shí)間的最大連接數(shù)量 ,默認(rèn)值是15 -->
<property name="maxPoolSize">20</property>
<!-- 任何指定時(shí)間的最小連接數(shù)量 ,默認(rèn)值是3 -->
<property name="minPoolSize">5</property>
</named-config>
</c3p0-config>
提供一個(gè)工具類(lèi)DBUtil以獲取,釋放連接:
<pre>
public class DBUtil {
private static ComboPooledDataSource cpds=null;
static{
cpds=new ComboPooledDataSource("mysql");
}
public static Connection getConnection(){
Connection connection=null;
try {
connection = cpds.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
public static void close(Connection conn,PreparedStatement pstmt,ResultSet rs){
try {
if(rs!=null){
rs.close();
}
if(pstmt!=null){
pstmt.close();
}
if(rs!=null){
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
</pre>
要特別注意的一點(diǎn)是:即使是使用連接池,使用完Connection后調(diào)用close方法,當(dāng)然這不意味著關(guān)閉與數(shù)據(jù)庫(kù)的TCP 連接,而是將連接還回到池中去,如果不close掉的話(huà),這個(gè)連接將會(huì)一直被占用,直到連接池中的連接耗盡為止。
使用JavaMail發(fā)送郵件
使用JavaMail發(fā)送郵件非常簡(jiǎn)單,也是三步曲:
- 創(chuàng)建連接對(duì)象javax.mail.Session
- 創(chuàng)建郵件對(duì)象 javax.mail.Message
- 發(fā)送郵件
直接看代碼,詳細(xì)的注釋在代碼中,MailUtil代碼如下:
public class MailUtil implements Runnable {
private String email;// 收件人郵箱
private String code;// 激活碼
public MailUtil(String email, String code) {
this.email = email;
this.code = code;
}
public void run() {
// 1.創(chuàng)建連接對(duì)象javax.mail.Session
// 2.創(chuàng)建郵件對(duì)象 javax.mail.Message
// 3.發(fā)送一封激活郵件
String from = "xxx@qq.com";// 發(fā)件人電子郵箱
String host = "smtp.qq.com"; // 指定發(fā)送郵件的主機(jī)smtp.qq.com(QQ)|smtp.163.com(網(wǎng)易)
Properties properties = System.getProperties();// 獲取系統(tǒng)屬性
properties.setProperty("mail.smtp.host", host);// 設(shè)置郵件服務(wù)器
properties.setProperty("mail.smtp.auth", "true");// 打開(kāi)認(rèn)證
try {
//QQ郵箱需要下面這段代碼,163郵箱不需要
MailSSLSocketFactory sf = new MailSSLSocketFactory();
sf.setTrustAllHosts(true);
properties.put("mail.smtp.ssl.enable", "true");
properties.put("mail.smtp.ssl.socketFactory", sf);
// 1.獲取默認(rèn)session對(duì)象
Session session = Session.getDefaultInstance(properties, new Authenticator() {
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("xxx@qq.com", "xxx"); // 發(fā)件人郵箱賬號(hào)、授權(quán)碼
}
});
// 2.創(chuàng)建郵件對(duì)象
Message message = new MimeMessage(session);
// 2.1設(shè)置發(fā)件人
message.setFrom(new InternetAddress(from));
// 2.2設(shè)置接收人
message.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
// 2.3設(shè)置郵件主題
message.setSubject("賬號(hào)激活");
// 2.4設(shè)置郵件內(nèi)容
String content = "<html><head></head><body><h1>這是一封激活郵件,激活請(qǐng)點(diǎn)擊以下鏈接</h1><h3><a href='http://localhost:8080/RegisterDemo/ActiveServlet?code="
+ code + "'>http://localhost:8080/RegisterDemo/ActiveServlet?code=" + code
+ "</href></h3></body></html>";
message.setContent(content, "text/html;charset=UTF-8");
// 3.發(fā)送郵件
Transport.send(message);
System.out.println("郵件成功發(fā)送!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
ps:需要把上面的賬號(hào)、授權(quán)碼進(jìn)行相應(yīng)修改。
完成后,再有用戶(hù)提交注冊(cè)信息時(shí),應(yīng)該就能收到驗(yàn)證郵件了:

用戶(hù)點(diǎn)擊鏈接后,我們要做的工作就是根據(jù)code(可以利用UUID生成)更改數(shù)據(jù)庫(kù)中相應(yīng)用戶(hù)的狀態(tài),然后提示用戶(hù)注冊(cè)結(jié)果了。
總結(jié)
簡(jiǎn)單介紹了如何使用JavaMail完成了一個(gè)帶郵箱驗(yàn)證的注冊(cè)案例,當(dāng)然在實(shí)際開(kāi)發(fā)中還有許多細(xì)節(jié)要注意,例如對(duì)用戶(hù)提交信息的校驗(yàn),密碼進(jìn)行加密等,此處的簡(jiǎn)單案例并未詳盡處理這些細(xì)節(jié)。