採用經典 Java EE (Servlet + JSP + JDBC + JavaMail) 分層架構:
CREATE TABLE `user` (
`id` CHAR(32) PRIMARY KEY, -- UUID 無連字號
`name` VARCHAR(30) NOT NULL,
`pwd` VARCHAR(20) NOT NULL,
`email` VARCHAR(50) NOT NULL,
`active` CHAR(1) DEFAULT '0', -- 0=未驗證, 1=已驗證
`acode` CHAR(32) -- 驗證碼 (UUID)
);
public class User {
private String id, name, pwd, email, active, acode;
// 標準 Getter/Setter、equals/hashCode (依 id)
}
public class C3p0Pool {
private static DataSource ds = new ComboPooledDataSource("hncu"); // 讀 c3p0-config.xml
private static ThreadLocal tl = new ThreadLocal<>();
public static Connection getCon() throws SQLException {
Connection con = tl.get();
if (con == null) { con = ds.getConnection(); tl.set(con); }
return con;
}
// 提供 DataSource 給 DbUtils 使用
public static DataSource getPool() { return ds; }
}
c3p0-config.xml 設定 `named-config name="hncu"`,含驅動、URL、帳密、池大小參數。
public class RegDaoJdbc implements RegDAO {
private QueryRunner qr = new QueryRunner(C3p0Pool.getPool());
public User reg(User user) {
String sql = "INSERT INTO user(id,name,pwd,email,active,acode) VALUES(?,?,?,?,?,?)";
String id = UUID.randomUUID().toString().replace("-", "");
String acode = UUID.randomUUID().toString().replace("-", "");
qr.update(sql, id, user.getName(), user.getPwd(), user.getEmail(), "0", acode);
user.setId(id); user.setAcode(acode); user.setActive("0");
return user;
}
public User active(String acode) {
User user = qr.query("SELECT * FROM user WHERE acode=?", new BeanHandler<>(User.class), acode);
if (user != null) {
qr.update("UPDATE user SET active='1', acode='' WHERE acode=?", acode);
}
return user;
}
}
public class RegService implements IRegService {
private RegDAO dao = new RegDaoJdbc();
public User reg(User user) { return dao.reg(user); }
public User active(String acode) { return dao.active(acode); }
}
@WebServlet("/RegServlet")
public class RegServlet extends HttpServlet {
private IRegService service = new RegService();
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
User user = new User();
user.setName(req.getParameter("name"));
user.setPwd(req.getParameter("pwd"));
user.setEmail(req.getParameter("email"));
user = service.reg(user);
if (user != null) {
resp.getWriter().print("註冊成功,請至信箱驗證");
new SendMailThread(user.getAcode()).start(); // 非同步發信
} else {
resp.getWriter().print("註冊失敗");
}
}
}
public class SendMailThread extends Thread {
private String acode;
private static final Logger log = Logger.getLogger(SendMailThread.class);
public SendMailThread(String acode) { this.acode = acode; }
public void run() {
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.sina.com");
props.put("mail.smtp.auth", "true");
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("your_email@sina.com", "password");
}
});
session.setDebug(true); // 除錯輸出 SMTP 交互
try {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress("your_email@sina.com"));
msg.setRecipient(Message.RecipientType.TO, new InternetAddress("user@qq.com")); // 實際應用用 user.getEmail()
msg.setSubject("啟用帳號");
StringBuilder sb = new StringBuilder();
sb.append("請點擊啟用: <a href='https://localhost:8080/mailWeb/ActiveServlet?acode=")
.append(acode).append("'>啟用連結</a><br/>");
sb.append("或複製: https://localhost:8080/mailWeb/ActiveServlet?acode=").append(acode);
msg.setContent(sb.toString(), "text/html;charset=utf-8");
Transport.send(msg);
log.info("郵件發送成功");
} catch (MessagingException e) {
log.error("郵件發送失敗", e);
}
}
}
@WebServlet("/ActiveServlet")
public class ActiveServlet extends HttpServlet {
private IRegService service = new RegService();
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String acode = req.getParameter("acode");
User user = service.active(acode);
req.setAttribute("user", user);
req.getRequestDispatcher("/jsps/result.jsp").forward(req, resp);
}
}
index.jsp:登入/註冊入口
reg.jsp:註冊表單 (姓名、密碼、Email)
result.jsp:啟用結果頁,5 秒倒數跳回首頁
| 議題 | 建議 |
|---|---|
| 密碼明文儲存 | **必須** 使用 BCrypt/Argon2 雜湊:`BCrypt.hashpw(pwd, BCrypt.gensalt())` |
| SQL 注入 | 已使用 DbUtils 參數化查詢,安全 |
| 驗證碼暴力破解 | UUID 32 字元空間極大,但建議加上**過期時間** (如 24 小時) |
| 郵件發送失敗重試 | 引入重試機制 (指數退避) 或訊息佇列 (RabbitMQ/Kafka) |
| 非同步執行緒管理 | 生產環境請用 **線程池** (ExecutorService) 而非直接 `new Thread()` |
| SMTP 憑證硬編碼 | 移至設定檔/環境變數/密碼管理系統 |
| CSRF 保護 | 表單加入 CSRF Token |
本系統示範了 Java EE 郵箱驗證註冊的完整閉環:
架構清晰、分層分明,是進階 Spring Boot、JPA、Thymeleaf、Redis 快取驗證碼、OAuth2 整合的絕佳基礎專案。