9、ssm整合activeMQ、JAVAMail實現郵件非同步註冊和登陸功能
1、前言
註冊某個網站的時候,往往要你用郵箱註冊,傳送郵件的功能很簡單,但是在點選註冊傳送郵件的時候,總不能等郵件傳送完畢之後才能跳轉頁面吧?或者說,我們應該將發郵件的這個過程異步出去,讓他自己慢慢去發郵件,我的主執行緒直接跳轉到其他頁面,等郵件到了,使用者點選啟用就可以使用了,傳送簡訊驗證碼也是同樣,這一篇利用activeMQ實現訊息佇列非同步傳送。
2、注意
必須先啟動activeMQ才能啟動專案
我這裡將會實現一個完整的從註冊-傳送郵件啟用-登陸的過程,所以程式碼量稍微有所提升,但是我在本地跑是沒有問題的,所以可以嘗試先跑起來再說。
關於郵件傳送,啟用這一塊,由於是本地測試,必須要放到外網才能跟實際一樣操作;我採用的方案是用花生殼軟體實現內網穿透和域名對映,模擬真實環境下的啟用過程;如果沒有條件或者不想搞,那就直接將資料庫中的標誌位改掉吧,這樣相當於啟用成功了。
先下載activeMQ,具體的使用可以參照我的有道雲筆記
3、效果預覽:
- 動態圖看不清的話,可以右擊複製圖片連結,開啟一個新的網頁重新開啟,顯示的應該比較清楚。
4、廢話不多說,開幹,先建立兩張表:
使用者表:
CREATE TABLE t_user(
`id` bigint(20) NOT NULL AUTO_INCREMENT ,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`headImgUrl` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`phoneNumber` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`register_date` datetime NOT NULL ,
`sex` tinyint(2) NULL DEFAULT NULL ,
`email` varchar(255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`role_Id` int(11) NOT NULL DEFAULT 1 ,
PRIMARY KEY (`id`)
);
登錄檔:
CREATE TABLE `t_register` (
`id` bigint(20) NOT NULL AUTO_INCREMENT ,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`state` tinyint(2) NULL DEFAULT NULL ,
`user_id` bigint(20) NOT NULL ,
PRIMARY KEY (`id`)
);
5、引入MQ和mail的依賴:
<!--MQ-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.13.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.13.2</version>
</dependency>
<!--java mail-->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
6、根據使用者表和登錄檔自動生成對應的實體類、dao介面、dao sql對映檔案吧。
此處略,怎麼做,之前整合ssm部落格中已經詳細介紹了。
7、想把頁面做的好看一點,先將我專案中的static下的所有檔案全部拷貝到自己工程相對應的地方吧!下面先寫一個註冊的jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="/WEB-INF/views/includes.jsp" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>AdminLTE 2 | Registration Page</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Bootstrap 3.3.6 -->
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<!-- Ionicons -->
<link rel="stylesheet" href="/static/css/ionicons.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="/static/dist/css/AdminLTE.min.css">
<!-- iCheck -->
<link rel="stylesheet" href="/static/plugins/iCheck/square/blue.css">
</head>
<body class="hold-transition register-page">
<div class="register-box">
<div class="register-logo">
<a href="../../index2.html"><b>註冊</b></a>
</div>
<div class="register-box-body">
<p class="login-box-msg">蝸牛生活</p>
<form action="/register/regist" method="post" id="registerFrom">
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="使用者名稱" name="username" id="username">
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span id="username_alert" style="color: red;visibility: hidden">使用者名稱不能為空</span>
</div>
<div class="form-group has-feedback">
<input type="email" class="form-control" placeholder="郵箱" name="email" id="email">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
<span id="email_alert" style="color: red;visibility: hidden">郵箱已存在</span>
<span id="email_style_alert" style="color: red;visibility: hidden">郵箱格式不正確</span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="密碼" name="password" id="password">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
<span id="password_alert" style="color: red;visibility: hidden">密碼不能為空</span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="確認密碼" name="rePassword" id="rePassword">
<span class="glyphicon glyphicon-log-in form-control-feedback"></span>
<span id="rePassword_alert" style="color: red;visibility: hidden">密碼不一致</span>
</div>
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="手機號碼" name="phoneNumber" id="phoneNumber">
<span class="glyphicon glyphicon-phone form-control-feedback"></span>
<span id="phoneNumber_alert" style="color: red;visibility: hidden">手機號碼不能為空</span>
</div>
<div class="row">
<div class="col-xs-8">
</div>
<!-- /.col -->
<div class="col-xs-4">
<button type="button" class="btn btn-primary btn-block btn-flat" onclick="submitForm();" id="submitBtn">立即註冊</button>
</div>
<div class="col-xs-12"> <span id="register_error" style="color: red;visibility: hidden">註冊失敗</span></div>
<!-- /.col -->
</div>
</form>
<a href="${pageContext.request.contextPath}/login" class="text-center">已有帳號,立即登陸</a>
</div>
<!-- /.form-box -->
</div>
<!-- /.register-box -->
<!-- jQuery 2.2.3 -->
<script src="/static/jquery/jquery-2.2.3.min.js"></script>
<!-- Bootstrap 3.3.6 -->
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<!-- iCheck -->
<script src="/static/plugins/iCheck/icheck.min.js"></script>
<script>
$(document).ready(function(){
verify = false;
checkEmail();
});
function checkEmail(){
$('#email').focusout(function(){
var email = $(this).val();
var param="${pageContext.request.contextPath}/register/checkEmail?email="+email+"";
$.ajax({
async:false,
url:param,
type:"POST",
dataType:"json",
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',//防止亂碼
success:function(data){
if(data.code == 400){
$("#email_alert").css("visibility","visible");
$("#email_style_alert").css("visibility", "hidden");
verify = false;
}else{
$("#email_alert").css("visibility","hidden");
verify=true;
}
}
});
});
}
function submitForm() {
var flag = true;
var username = $("#username").val();
var email = $("#email").val();
var password = $("#password").val();
var rePassword = $("#rePassword").val();
var phoneNumber = $("#phoneNumber").val();
if(username.length==0){
$("#username_alert").css("visibility","visible");
flag=false;
}else {
$("#username_alert").css("visibility","hidden");
}
if(email.length==0){
$("#register_error").css("visibility","visible");
flag=false;
}else {
var reg = /^([a-zA-Z0-9_-])[email protected]([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/;
if(!reg.test(email)){
$("#email_style_alert").css("visibility","visible");
flag=false;
}else {
$("#email_style_alert").css("visibility", "hidden");
$("#register_error").css("visibility","hidden");
}
}
if(password.length==0){
$("#password_alert").css("visibility","visible");
flag=false;
}else {
$("#password_alert").css("visibility","hidden");
}
if(rePassword.length==0){
flag=false;
}else {
if(rePassword!=password){
$("#rePassword_alert").css("visibility","visible");
flag=false;
}else{
$("#rePassword_alert").css("visibility","hidden");
}
}
if(phoneNumber.length==0){
$("#phoneNumber_alert").css("visibility","visible");
flag=false;
}else {
$("#phoneNumber_alert").css("visibility","hidden");
}
if(verify==true&&flag==true){
$("#submitBtn").attr("disabled", "disabled").text("提交中...");
$("#registerFrom").submit();
}else {
$("#register_error").css("visibility","visible");
}
};
</script>
</body>
</html>
這裡主要就是表單驗證和提交嘛,相應的controler是:
@RequestMapping("")
public String register() {
return "/register/register";
}
//獲取引數,沒什麼問題就進行註冊,即插入值
@RequestMapping("/regist")
public String regist(HttpServletRequest request) throws UnsupportedEncodingException, NoSuchAlgorithmException {
try{
System.out.println("===================regist=========================");
String username = request.getParameter("username");
String password = request.getParameter("password");
String rePassword = request.getParameter("rePassword");
String email = request.getParameter("email");
String phoneNumber = request.getParameter("phoneNumber");
if(StringUtils.isBlank(username) || StringUtils.isBlank(password) || StringUtils.isBlank(rePassword) || StringUtils.isBlank(email) || StringUtils.isBlank(phoneNumber)){
return null;
}
//將收到的註冊資訊存到資料庫中,並且傳送啟用郵件,使用者未點選啟用郵件,狀態設為0,表示不可用,點選之後設為1,表示使用者可用。
registerService.addNewUser(username,password,rePassword,email,phoneNumber);
}catch (Exception e){
log.error("註冊失敗",e);
throw e;
}
return "/register/tip";
}
//檢查郵箱是否已經被使用
@RequestMapping("/checkEmail")
@ResponseBody
public Result checkEmail(HttpServletRequest request){
Result result = new Result();
String email = request.getParameter("email");
if(StringUtils.isBlank(email)){
result.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
return result;
}
// 從資料庫查詢是否存在這個郵箱
try{
if(!registerService.isUserOld(email)){
//說明這個郵箱已經被註冊了
result.setMessgae("郵箱已經被註冊");
result.setCode(Constants.RESP_STATUS_BADREQUEST);
}
} catch (Exception e){
result.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
result.setMessgae("未知錯誤");
}
return result;
}
addNewUser方法:
@Override
@Transactional
public void addNewUser(String username, String password, String rePassword, String email, String phoneNumber) throws UnsupportedEncodingException, NoSuchAlgorithmException {
User user = new User();
user.setUsername(username);
user.setEmail(email);
if(password.equals(rePassword)){
user.setPassword(MD5Util.encryptPassword(password));
}
user.setPhonenumber(phoneNumber);
/*頭像為預設,性別也預設為男*/
user.setHeadimgurl("snail.jpg");
user.setSex((byte)1);
user.setRegisterDate(new Date());
userMapper.insertSelective(user);
Register register = new Register();
register.setUserId(user.getId());
//使用者一點提交時,狀態肯定是0,即未認證的情況
register.setState((byte)0);
//code也隨機生成
String code = UUID.randomUUID().toString().replace("-", "");
register.setCode(code);
registerMapper.insertSelective(register);
//傳送啟用郵件
Destination destination = new ActiveMQQueue(SMS_QUEUE);
Map<String,String> emailParam = new HashMap<>();
emailParam.put("email",email);
emailParam.put("code", code);
emailParam.put("username",username);
String message = JSON.toJSONString(emailParam);
emailProcessor.sendEmaillToQueue(destination,message);
}
@Override
public Register getRegisterByCode(String code) throws MyException {
Register register = registerMapper.getRegisterByCode(code);
if(register==null){
throw new MyException("不存在使用者的啟用記錄");
}
return register;
}
首先是給使用者表塞值,然後給登錄檔塞值,最後傳送郵件。注意這裡有個小插曲:
在給使用者表塞值後,登錄檔需要這個使用者的id,需要在插入使用者的地方增加:
useGeneratedKeys="true" keyProperty="id"
這樣插入一條記錄之後將會返回制定的主鍵的值。
注意SMS_QUEUE這個常量:
private static final String SMS_QUEUE = "email.queue";
不是亂配的,這個表示佇列的名字,一個是生產者,一個是消費者,都對應這個佇列,在soring-cfg.xml中配置MQ的生產者和消費者:
<!--連結到amq-->
<amq:connectionFactory id="amqConnectionFactory" brokerURL="tcp://localhost:61616" userName="admin" password="admin" />
<!--交給spring管理-->
<bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory">
<property name="targetConnectionFactory" ref="amqConnectionFactory"/>
<property name="sessionCacheSize" value="10"/>
</bean>
<!-- 配置JMS模版 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<!-- 如果為True,則是Topic;如果是false或者預設,則是queue -->
<property name="pubSubDomain" value="false"/>
</bean>
<jms:listener-container destination-type="queue" connection-factory="connectionFactory">
<jms:listener destination="email.queue" ref="doSendSmsMessage"/>
</jms:listener-container>
注意不要在頭部引入約束檔案:
xmlns:jms="http://www.springframework.org/schema/jms"
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms-4.0.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.8.0.xsd
我們注意到配置了消費者監聽器一直監聽email.queue這個佇列,然後執行傳送訊息的方法。
好,下面我們來實現emailProcessor.sendEmaillToQueue(destination,message);
@Component
public class EmailProcessor {
@Autowired
private JmsTemplate jmsTemplate;
public void sendEmaillToQueue(Destination destination, String message) {
System.out.println("---------------------------------------生產----");
jmsTemplate.convertAndSend(destination, message);
}
}
這裡就實現了將訊息傳送到email.queue這個隊列了,下面就是消費這個訊息:
@Component("doSendSmsMessage")
public class DoSendEmail implements MessageListener {
@Autowired
private EmailSender emailSender;
@Override
public void onMessage(Message message) {
TextMessage textMsg = (TextMessage) message;
System.out.println("---------------------------------------消費開始----");
try {
JSONObject jsonObject = JSON.parseObject(textMsg.getText());
emailSender.sendEmail(jsonObject.getString("email"),jsonObject.getString("code"),jsonObject.getString("username"));
} catch (JMSException e) {
e.printStackTrace();
}
System.out.println("---------------------------------------消費結束----");
}
}
我們可以看到這就是上面配置檔案提到的doSendSmsMessage,這裡實現接受訊息然後傳送出去,具的傳送郵件的方法emailSender.sendEmail是這樣實現的:
@Component
public class EmailSender {
public void sendEmail(String to, String code, String username) {
try {
Properties props = new Properties();
props.put("username", "你的qq號");
props.put("password", "你的密碼");
props.put("mail.transport.protocol", "smtp" );
props.put("mail.smtp.host", "smtp.qq.com");
props.put("mail.smtp.port", "465" );
props.put("mail.smtp.auth","true");
props.put("mail.smtp.ssl.enable","true");
Session mailSession = Session.getDefaultInstance(props);
Message msg = new MimeMessage(mailSession);
msg.setFrom(new InternetAddress("[email protected]"));
msg.addRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
msg.setSubject("啟用郵件");
msg.setContent(username+",您好,恭喜你成功註冊本系統。點選連結即可完成啟用。<h1>此郵件為蝸牛生活傳送的啟用郵件!請點選下面連結完成啟用操作!</h1><h3><a href='http://1dm8911156.imwork.net/register/active?code="+code+"'>《---蝸牛生活---》</a></h3>","text/html;charset=UTF-8");
msg.saveChanges();
Transport transport = mailSession.getTransport("smtp");
transport.connect(props.getProperty("mail.smtp.host"), props
.getProperty("username"), props.getProperty("password"));
transport.sendMessage(msg, msg.getAllRecipients());
transport.close();
} catch (Exception e) {
e.printStackTrace();
System.out.println(e);
}
}
}
總之寫自己的真實登陸密碼肯定是不行的,校驗不通過。
這樣,非同步傳送郵件的整個過程算是走完了。我只寫了關鍵部分,沒有涵蓋全部,具體看我的程式碼,畢竟思路最重要嘛。
註冊成功之後,跳轉到一個頁面提示註冊成功tip.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="/WEB-INF/views/includes.jsp" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>提示</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Bootstrap 3.3.6 -->
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<!-- Ionicons -->
<link rel="stylesheet" href="/static/css/ionicons.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="/static/dist/css/AdminLTE.min.css">
<!-- jQuery 2.2.3 -->
<script src="/static/jquery/jquery-2.2.3.min.js"></script>
</head>
<body class="hold-transition lockscreen">
<!-- Automatic element centering -->
<div class="lockscreen-wrapper">
<div class="lockscreen-logo">
<a href="../../index2.html"><b>蝸牛生活</b></a>
</div>
<div class="lockscreen-name">註冊成功</div>
<div>
<a href="${basePath}/login"><span id="time">5</span>秒後系統會自動跳轉到登陸頁面,也可點選本處直接跳轉哦</a>
<script type="text/javascript">
$(document).ready(function(){
delayURL("${basePath}/login");
})
function delayURL(url) {
var delay = document.getElementById("time").innerHTML;
if (delay > 0) {
delay--;
document.getElementById("time").innerHTML = delay
} else {
window.top.location.href = url
}
setTimeout("delayURL('" + url + "')", 1000)
}
</script>
</div>
</div>
<!-- /.center -->
</body>
</html>
8、啟用
這就很簡單的,根據code獲取這個註冊的記錄,將記錄中的標誌位修改一下,就表示激活了。
說到這個code,我可能上面註冊的步驟中沒有是說明,他就是一個隨機字串,然後將他放倒郵件內容中,使用者點選了,我就能獲取這個字串,也就找到了使用者本身,我就能修改使用者的啟用狀態了,簡而言之,我總要有個東西明確一下是哪個使用者激活了哪個郵箱啊。
@RequestMapping(value = "/active",method = RequestMethod.GET)
public String active(@RequestParam String code) throws MyException {
try{
if(StringUtils.isBlank(code)){
log.error("使用者code為空");
throw new MyException("使用者啟用郵件的引數有問題");
}
// 根據code獲取該使用者資訊,將狀態置為1即啟用
/*先根據code獲取註冊資訊,拿到使用者id*/
Register r = registerService.getRegisterByCode(code);
if(r.getState()==0){
r.setState((byte)1);
registerService.updateStatus(r);
return "/register/active";
}else{
return "/register/fail";
}
}catch (Exception e){
log.error("啟用失敗",e);
throw e;
}
}
啟用成功之後,直接跳轉到啟用成功提示頁面active.jsp:
<%@ page contentType="