Ch10-3 : Sigup
[Signup]
這個小節,會詳細說明註冊功能怎麼實作出來的,我把註冊的動作,拆解成七個部份功能,依序做好這七個部份功能後,我們就可以完成註冊功能了。若解釋的過程中,還是不太明白,建議實際針對每個步驟去操作,理解每個步驟的運作流程,就會更加印象深刻了。
- Step 1 : 取得表單註冊資料,若錯誤,回到註冊頁面,警告錯誤訊息。
- Step 2 : 進行表單驗證,是否正確。若錯誤,回到註冊頁面顯示錯誤訊息。
- Step 3 : 檢核通過,新增會員資料,且尚未認證。
- Step 4 : 註冊新增成功,新增認證連結資料。
- Step 5 : 新增會員記錄檔。
- Step 6 : 進行寄送認證信動作。
- Step 7 : 以上都順利完成,導入成功註冊頁面。
網址 : https://github.com/loveu8/playMyBtaisMariaDB/blob/Ch10/app/controllers/WebController.java
Step 0
首先,我們先把上個章節,已經擺好的網頁與檔案,拿出來檢視註冊功能的畫面是怎樣完成。
app/views/web/headerLibs.scala.html
之後會有許多頁面會需要到這些css與js,都需要引用到,特別獨立成Libary頁面。
<!-- JS lib-->
<script src="/assets/javascripts/jquery-3.1.0.min.js"></script>
<script src="/assets/javascripts/jquery.dropotron.min.js"></script>
<script src="/assets/javascripts/skel.min.js"></script>
<script src="/assets/javascripts/skel-viewport.min.js"></script>
<script src="/assets/javascripts/util.js"></script>
<script src="/assets/javascripts/main.js"></script>
<!-- 全域樣式 -->
<link rel="stylesheet" href="/assets/stylesheets/main.css" />
<link rel="stylesheet" href="/assets/stylesheets/normalize.css">
/app/views/web/loginSignup/loginSignupLibs.scala.html
註冊與登入的css檔案。
<link rel="stylesheet" href='@routes.Assets.versioned("stylesheets/loginSignup.css")'>
app/views/web/headerNav.scala.html
Header上方的選單列表,因為很經常被各個頁面使用到,也獨立出來,方面之後其它頁面引用。
<!-- Nav -->
<nav id="nav">
<ul>
<li id="nav_index"><a href="@controllers.routes.WebController.index.url">首頁</a></li>
<li id="nav_user">
<a href="#">我的世界</a>
<ul>
<li><a href="@controllers.routes.WebController.login.url">登入</a></li>
</ul>
</li>
<li id="nav_signup">
<a href="@controllers.routes.WebController.signup.url">註冊</a>
<ul>
<li><a href="@controllers.routes.WebController.resendAuthEmail.url">重發認證信</a></li>
</ul>
</li>
</ul>
</nav>
<script>
<!-- 來偵測,外面的Div被誰使用到,而去增加class current屬性-->
$(document).ready(function() {
if ( $('#select_nav_index').length ){
$('#nav_index').addClass('current');
}
if ( $('#select_nav_user').length ){
$('#nav_user').addClass('current');
}
if ( $('#select_nav_signup').length ){
$('#nav_signup').addClass('current');
}
});
</script>
<a href="index.html" id="logo">Star</a>
conf/routes
# http://127.0.0.1:9000/web/signup
GET /web/signup controllers.WebController.signup()
# http://127.0.0.1:9000/web/signupOk (測試用)
GET /web/signupOk controllers.WebController.signupOk()
# http://127.0.0.1:9000/web/signup
POST /web/signup controllers.WebController.goToSignup()
app/views/web/loginSignup/signup.scala.html
註冊頁面。
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>註冊</title>
@*利用Play Scala的小老鼠符號,來去引用相關頁面*@
@views.html.web.headerLibs()
@views.html.web.loginSignup.loginSignupLibs()
</head>
<body>
<div id="page-wrapper">
<div id="select_nav_signup">@views.html.web.headerNav()</div>
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a href="#signup">註冊</a></li>
</ul>
<div class="tab-content">
<div id="signup">
<form action="@controllers.routes.WebController.goToSignup.url" method="post" id="signupForm">
<div class="field-wrap">
<label class="lable-field-wrap">使用者名稱</label>
<input type="text" required autocomplete="off" name="username"/>
@if(flash.containsKey("username")) {
<span style="color:red;">@flash.get("username")</span>
}
</div>
<div class="field-wrap">
<label class="lable-field-wrap">電子信箱</label>
<input type="email" required autocomplete="off" name="email"/>
@if(flash.containsKey("email")) {
<span style="color:red;">@flash.get("email")</span>
}
</div>
<div class="field-wrap">
<label class="lable-field-wrap">密碼</label>
<input type="password" required autocomplete="off" name="password"/>
</div>
<div class="field-wrap">
<label class="lable-field-wrap">確認密碼</label>
<input type="password" required autocomplete="off" name="retypePassword"/>
@if(flash.containsKey("password")) {
<span style="color:red;">@flash.get("password")</span>
}
@if(flash.containsKey("signupError")) {
<span style="color:red;">@flash.get("signupError")</span>
}
</div>
<a>
<input type="submit" value="Signup" class="button button-block">
</a>
</form>
</div>
</div><!-- tab-content -->
</div> <!-- /form -->
<div id="titleBar"></div>
<script src='@routes.Assets.versioned("javascripts/loginSignup.js")'></script>
<script>
$('#signupForm')[0].reset();
@if(flash.containsKey("errorForm")) {
alert('@flash.get("errorForm")');
}
</script>
</body>
</html>
/app/views/web/loginSignup/signupOk.scala.html
註冊成功畫面。
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>註冊成功</title>
@views.html.web.headerLibs()
@views.html.web.loginSignup.loginSignupLibs()
</head>
<body>
<div id="page-wrapper">
<div id="select_nav_signup">@views.html.web.headerNav()</div>
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a href="#">註冊成功</a></li>
</ul>
<div class="tab-content">
<div id="signup">
<span>您已註冊成功,請至您申請的信箱,收取認證信件,認證有效時間為24小時內,謝謝。</span>
</div>
</div><!-- tab-content -->
</div> <!-- /form -->
<div id="titleBar"></div>
</body>
</html>
參考畫面
網址 : http://127.0.0.1:9000/web/signup
接下來回到我們的WebController,註冊會員是這個以下說明流程順序,下面會依序講解註冊步驟。
path : /app/controllers/WebController.java
/**
* <pre>
* 進行註冊
*
* Step 1 : 取得表單註冊資料,若錯誤,回到註冊頁面,警告錯誤訊息。
* Step 2 : 進行表單驗證,是否正確。若錯誤,回到註冊頁面顯示錯誤訊息。
* Step 3 : 檢核通過,新增會員資料,且尚未認證。
* Step 4 : 註冊新增成功,新增認證連結資料。
* Step 5 : 新增會員記錄檔。
* Step 6 : 進行寄送認證信動作。
* Step 7 : 以上都順利完成,導入成功註冊頁面。
*
* </pre>
*/
public Result goToSignup() {
// 清除暫存錯誤訊息
flash().clear();
// Step 1
SignupRequest request = this.getSignupRequest();
if (request == null) {
flash().put("errorForm","註冊資料錯誤,請重新嘗試!!");
return ok(signup.render());
}
// Step 2
Map<String, VerificFormMessage> verificInfo = this.checkSingupRequest(request);
for (String key : verificInfo.keySet()) {
// 發現驗證沒過,放入錯誤訊息
if (!"200".equals(verificInfo.get(key).getStatus())) {
flash().put(key, verificInfo.get(key).getStatusDesc());
}
}
if(!flash().isEmpty()){
return ok(signup.render());
}
try {
// Step 3
int isSignupOk = webService.signupNewMember(request);
if(isSignupOk == 0){
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
// Step 4
Utils_Signup utils_Signup = new Utils_Signup();
Member newMember = webService.findMemberByEmail(request.getEmail());
String signupAuthString = utils_Signup.genSignupAuthString(newMember.getEmail());
Map<String , String> memberToken = new HashMap<String , String>();
memberToken.put("memberNo", newMember.getMemberNo());
memberToken.put("tokenString", signupAuthString);
memberToken.put("type", MemberTokenType.Signup.toString());
int isSingAuthStringOk = webService.genSignupAuthData(memberToken);
// Step 5
Map<String , String> memberLoginData
= utils_Signup.genMemberLoginData(newMember.getMemberNo() ,
"PC" ,
request().remoteAddress() ,
MemberLoginStatus.S1.getStatus());
int isMemberLoginLogOk = webService.genMemberLoginLog(memberLoginData);
// Step 6
Utils_Email utils_Email = new Utils_Email();
Email email = utils_Email.genSinupAuthEmail(newMember, signupAuthString);
boolean isSeadMailOk = utils_Email.sendMail(email);
// Step 7
if(isSingAuthStringOk > 0 && isMemberLoginLogOk > 0 && isSeadMailOk ){
return ok(signupOk.render());
} else {
flash().put("signupError", "Opss...寄送認證信件發生錯誤,請使用重發認證信功能,完成認證動作,謝謝。");
return ok(signup.render());
}
} catch (Exception e) {
e.printStackTrace();
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
}
// Step 1 : 取得註冊資訊請求
private SignupRequest getSignupRequest() {
SignupRequest request = null;
try {
request = formFactory.form(SignupRequest.class).bindFromRequest().get();
Logger.info("before , new member request data = " + Json.toJson(request));
} catch (Exception e) {
Logger.error("表單內容非註冊資訊,轉換類別錯誤,回傳空物件");
}
return request;
}
// Step 2 : 檢查註冊資訊
private Map<String, VerificFormMessage> checkSingupRequest(SignupRequest request) {
boolean isRegEmail = true;
boolean isUsedUsername = true;
try{
isRegEmail = webService.checkMemberByEmail(request.getEmail());
isUsedUsername = webService.checkMemberByUsername(request.getUsername());
} catch(Exception e){
e.printStackTrace();
}
Map<String, VerificFormMessage> verificInfo
= new Utils_Signup().checkSingupRequest(request , isRegEmail , isUsedUsername);
Logger.info("verificInfo = " + Json.toJson(verificInfo));
return verificInfo;
}
Step 1 : 取得表單註冊資料,若錯誤,回到註冊頁面,警告錯誤訊息。
會需要表單填寫的原因是,可能會有不正當的使用者,想利用傳送表單時,塞入不屬於這個表單的內容,嘗試攻擊網站,我們需要針對註冊申請表單做檢核,確認是否市申請註冊的表單資料。
path : /app/controllers/WebController.java
...
public Result goToSignup() {
// 清除暫存錯誤訊息
flash().clear();
// Step 1
SignupRequest request = this.getSignupRequest();
if (request == null) {
flash().put("errorForm","註冊資料錯誤,請重新嘗試!!");
return ok(signup.render());
}
...
}
// Step 1 : 取得註冊資訊請求
private SignupRequest getSignupRequest() {
SignupRequest request = null;
try {
request = formFactory.form(SignupRequest.class).bindFromRequest().get();
Logger.info("before , new member request data = " + Json.toJson(request));
} catch (Exception e) {
Logger.error("表單內容非註冊資訊,轉換類別錯誤,回傳空物件");
}
return request;
}
檢查畫面。
彈跳警告畫面,主要寫在signup.scala.html裡。當回到原本頁面,flash發現含有errorForm的資料,就會顯示出要給使用者知道的訊息,提醒使用者不要使用不合法資料,攻擊網站。
/app/views/web/loginSignup/signup.scala.html
<script>
$('#signupForm')[0].reset();
@if(flash.containsKey("errorForm")) {
alert('@flash.get("errorForm")');
}
</script>
Step 2 : 進行表單驗證,是否正確。若錯誤,回到註冊頁面顯示錯誤訊息。
前面確認過使用者輸入的資料,是我們想要的內容後,接下來我們要針對使用者輸入的資料進行驗證。
首先新增表單對應的類別,這樣我們Play就可以對註冊表單進行Form bind,轉換成物件來操作。
path : /app/pojo/web/signup/request/SignupRequest.java
package pojo.web.signup.request;
public class SignupRequest {
private String email;
private String username;
private String password;
private String retypePassword;
public void setEmail(String email) {
this.email = email;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setRetypePassword(String retypePassword) {
this.retypePassword = retypePassword;
}
public String getEmail() {
return email;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getRetypePassword() {
return retypePassword;
}
}
path : /app/controllers/WebController.java
@Inject
FormFactory formFactory;
@Inject
private WebService webService;
public Result goToSignup() {
...
// Step 2
Map<String, VerificFormMessage> verificInfo = this.checkSingupRequest(request);
for (String key : verificInfo.keySet()) {
// 發現驗證沒過,放入錯誤訊息
if (!"200".equals(verificInfo.get(key).getStatus())) {
flash().put(key, verificInfo.get(key).getStatusDesc());
}
}
if(!flash().isEmpty()){
return ok(signup.render());
}
...
}
...
// Step 2 : 檢查註冊資訊
private Map<String, VerificFormMessage> checkSingupRequest(SignupRequest request) {
boolean isRegEmail = true;
boolean isUsedUsername = true;
try{
isRegEmail = webService.checkMemberByEmail(request.getEmail());
isUsedUsername = webService.checkMemberByUsername(request.getUsername());
} catch(Exception e){
e.printStackTrace();
}
Map<String, VerificFormMessage> verificInfo
= new Utils_Signup().checkSingupRequest(request , isRegEmail , isUsedUsername);
Logger.info("verificInfo = " + Json.toJson(verificInfo));
return verificInfo;
}
因為註冊需要跟資料庫做新增刪修的動作,我們需要寫好相關的程式,來紀錄相關資訊。跟之前一樣,需要先建立一個Interface,寫好對資料庫操作的各種行為。
path : /app/services/WebService.java
package services;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import pojo.web.Member;
import pojo.web.MemberToken;
import pojo.web.signup.request.SignupRequest;
public interface WebService {
/** 註冊新會員 */
public int signupNewMember(@Param("signupRequest") SignupRequest signupRequest);
/** 檢查是否有該會員存在 */
public boolean checkMemberByEmail(String email);
/** 檢查是否有該會員使用者名稱是否存在 */
public boolean checkMemberByUsername(String username);
/** 用Email 尋找會員資料 */
public Member findMemberByEmail(String email);
/** 用會員編號 尋找會員資料 */
public Member findMemberByMemberNo(String memberNo);
/** 用Email與使用者名稱 尋找會員資料 */
public Member findMemberByEmailAndUserName(@Param("email")String email , @Param("username")String username);
/** 產生會員Token資料 */
public int genTokenData(@Param("memberToken") Map<String , String> memberToken);
/** 會員記錄檔 */
public int genMemberLoginLog(@Param("memberLoginData") Map<String , String> memberLoginData);
/** 驗證會員連結 */
public MemberToken getMemberTokenData(@Param("token") String token , @Param("type") String type);
/** 更新認證連結*/
public int updateMemberAuth(String memberNo);
/** 更新會員資料*/
public int updateMemberToAuthOk(String memberNo);
/** 新增會員紀錄檔資料 */
public int genMemberChangeLog(@Param("member") Member member);
}
有interface後,我們就需要新增對應的WebService.xml,依照interface的定義,寫好相關的SQL程式。
path : /conf/services/WebService.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="services.WebService">
<!-- 註冊會員 -->
<insert id="signupNewMember" parameterType="pojo.web.signup.request.SignupRequest">
insert into
member_main (memberNo ,email , password , username , createDate, modifyDate)
values
(
null ,
#{signupRequest.email} ,
#{signupRequest.password},
#{signupRequest.username} ,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s') ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
);
</insert>
<!-- 檢查是否有重覆註冊的會員 -->
<select id="checkMemberByEmail" parameterType="String"
resultType="boolean">
SELECT count(1) FROM member_main WHERE email=#{email}
</select>
<!-- 檢查是否有重覆的使用者名稱 -->
<select id="checkMemberByUsername" parameterType="String"
resultType="boolean">
SELECT count(1) FROM member_main WHERE username=#{username}
</select>
<!-- 根據email查詢會員資料 -->
<select id="findMemberByEmail" parameterType="String"
resultType="pojo.web.Member">
SELECT * FROM member_main WHERE email = #{email}
</select>
<!-- 根據memberNo查詢會員資料 -->
<select id="findMemberByMemberNo" parameterType="String"
resultType="pojo.web.Member">
SELECT * FROM member_main WHERE memberNo = #{memberNo}
</select>
<!-- 根據 email與username查尋會員資料 -->
<select id="findMemberByEmailAndUserName" parameterType="String"
resultType="pojo.web.Member">
SELECT * FROM member_main WHERE email = #{email} AND username =#{username}
</select>
<!-- 註冊認證連結 -->
<insert id="genTokenData" parameterType="Map">
insert into
member_token (tokenString, type ,memberNo , sendDate , isUse , createDate , modifyDate , expiryDate)
values
(
#{memberToken.tokenString} ,
#{memberToken.memberNo} ,
#{memberToken.type} ,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s'),
0,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s') ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') ,
DATE_FORMAT(DATE_ADD(NOW(),INTERVAL 1 DAY),'%Y%m%d%H%i%s')
)
</insert>
<!-- 會員記錄檔 -->
<insert id="genMemberLoginLog" parameterType="Map">
insert into
member_login_log (memberNo , status , ipAddress , device , loginDate)
values
(
#{memberLoginData.memberNo} ,
#{memberLoginData.status} ,
#{memberLoginData.ipAddress} ,
#{memberLoginData.device} ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
);
</insert>
<!-- 撈出會員認證資訊 -->
<select id="getMemberTokenData" parameterType="String" resultType="pojo.web.MemberAuth">
SELECT *,DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') as dbTime FROM member_auth WHERE authString=#{auth}
</select>
<!-- 更新會員認證-->
<update id="updateMemberToken">
UPDATE member_token SET isUse = 1 , modifyDate = DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
WHERE memberNo = #{memberNo} AND isUse = 0 AND type=#{type}
</update>
<!-- 更新會員資料 -->
<update id="updateMemberToAuthOk">
UPDATE member_main SET status = '2' , modifyDate = DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
WHERE memberNo = #{memberNo}
</update>
<!-- 會員記錄檔 -->
<insert id="genMemberChangeLog" parameterType="pojo.web.Member">
insert into
member_main_log(memberNo , email , password , username , createDate)
values
(
#{member.memberNo} ,
#{member.email} ,
#{member.password},
#{member.username} ,
DATE_FORMAT(NOW(), '%Y%m%d%H%i%s')
);
</insert>
</mapper>
前面兩個檔案就緒之後,接下來調整MyBatisModule.java,把WebService.java加入進去,這樣我們Play就可以跟MariaDB互動了。
path : /app/modules/MyBatisModule.java
...
@Override
protected void initialize() {
environmentId("default");
bindConstant().annotatedWith(Names.named("mybatis.configuration.failFast")).to(true);
bindDataSourceProviderType(PlayDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
// 把我們要呼叫DB的類別,增加到MapperClass
addMapperClass(UserService.class);
addMapperClass(WebService.class);
}
....
前面完成後,我們新增一隻Utils_Signup來檢查申請註冊的資料。
path : /app/utils/signup/Utils_Signup.java
package utils.signup;
import java.security.MessageDigest;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import play.libs.Json;
import pojo.web.signup.request.SignupRequest;
import pojo.web.signup.status.EmailStatus;
import pojo.web.signup.status.PasswordStatus;
import pojo.web.signup.status.UsernameStatus;
import pojo.web.signup.verific.VerificFormMessage;
public class Utils_Signup {
public Map<String , VerificFormMessage>checkSingupRequest(SignupRequest reqeuest , boolean isRegEmail , boolean isUsedUsername){
Map<String , VerificFormMessage> info = new HashMap<String , VerificFormMessage>();
String email = reqeuest.getEmail();
String username = reqeuest.getUsername();
String password = reqeuest.getPassword();
String retypepassword = reqeuest.getRetypePassword();
info.put("email", checkEmail(email , isRegEmail));
info.put("username", checkUsername(username , isUsedUsername));
info.put("password", checkPassword(password , retypepassword));
return info;
}
public VerificFormMessage checkEmail(String email , boolean isRegEmail){
VerificFormMessage message = new VerificFormMessage();
message.setInputName("email");
String emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$";
if("".equals(email) || email == null){
message.setStatus(EmailStatus.S1.status);
message.setStatusDesc(EmailStatus.S1.statusDesc);
} else if(! email.matches(emailRegex)){
message.setStatus(EmailStatus.S2.status);
message.setStatusDesc(EmailStatus.S2.statusDesc);
} else if(isRegEmail){
message.setStatus(EmailStatus.S3.status);
message.setStatusDesc(EmailStatus.S3.statusDesc);
} else {
message.setStatus(EmailStatus.S200.status);
message.setStatusDesc(EmailStatus.S200.statusDesc);
}
play.Logger.info("checkEmail = " + Json.toJson(message));
return message;
}
private VerificFormMessage checkUsername(String username , boolean isUsedUsername) {
VerificFormMessage message = new VerificFormMessage();
message.setInputName("username");
String usernameRegex = "^[a-zA-Z]{4,15}$";
if("".equals(username) || username == null){
message.setStatus(UsernameStatus.S1.status);
message.setStatusDesc(UsernameStatus.S1.statusDesc);
} else if(!username.matches(usernameRegex)){
message.setStatus(UsernameStatus.S2.status);
message.setStatusDesc(UsernameStatus.S2.statusDesc);
} else if(isUsedUsername){
message.setStatus(UsernameStatus.S3.status);
message.setStatusDesc(UsernameStatus.S3.statusDesc);
} else {
message.setStatus(UsernameStatus.S200.status);
message.setStatusDesc(UsernameStatus.S200.statusDesc);
}
play.Logger.info("checkUsername = " + Json.toJson(message));
return message;
}
private VerificFormMessage checkPassword(String password, String retypepassword) {
VerificFormMessage message = new VerificFormMessage();
message.setInputName("password");
String passwordRegex = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{4,15}$";
if("".equals(password) || password == null){
message.setStatus(PasswordStatus.S1.status);
message.setStatusDesc(PasswordStatus.S1.statusDesc);
} else if("".equals(retypepassword) || retypepassword == null){
message.setStatus(PasswordStatus.S2.status);
message.setStatusDesc(PasswordStatus.S2.statusDesc);
} else if(!retypepassword.equals(password)){
message.setStatus(PasswordStatus.S3.status);
message.setStatusDesc(PasswordStatus.S3.statusDesc);
} else if(!password.matches(passwordRegex)){
message.setStatus(PasswordStatus.S4.status);
message.setStatusDesc(PasswordStatus.S4.statusDesc);
} else {
message.setStatus(PasswordStatus.S200.status);
message.setStatusDesc(PasswordStatus.S200.statusDesc);
}
play.Logger.info("checkPassword = " + Json.toJson(message));
return message;
}
/**
* 產生註冊sha-256認證字串
* @param email 使用者信箱
* @return SHA256 String
*/
public String genSignupAuthString(String email){
Format formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String time = formatter.format(new Date());
String text = email + time;
return this.genSHA256String(text);
}
/**
* 傳入主要的需要認證字串,進行Sha256加密
* @param text
* @return token
*/
private String genSHA256String(String text){
String token = "";
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest();
md.update(text.getBytes("UTF-8"));
// %064x 意思是,產生64個字串長的字串
token = String.format("%064x", new java.math.BigInteger(1, digest));
} catch (Exception e) {
e.printStackTrace();
}
return token;
}
/**
* 產生會員記錄檔
* @param memberNo
* @param device
* @param ipAddress
* @param status
* @return Map<String , String>
*/
public Map<String , String> genMemberLoginData(String memberNo , String device , String ipAddress , String status){
Map<String , String> memberLoginLog = new HashMap<String , String>();
memberLoginLog.put("memberNo", memberNo);
memberLoginLog.put("device",device);
memberLoginLog.put("ipAddress",ipAddress);
memberLoginLog.put("status",status);
return memberLoginLog;
}
}
進入檢查使用者輸入的資料時,會先呼叫webService.checkMemberByEmail與webService.checkMemberByUsername,檢查是否有重複資料在資料庫裡。
接下來才去呼叫Utils_Signup.checkSingupRequest的方法。帶入以下三個參數。
SignupRequest : 使用者註冊的資料。
isRegEmail : 是否是註冊過的信箱。(webService.checkMemberByEmail的查詢結果)
isUsedUsername : 是否是註冊過的使用者名稱。(webService.checkMemberByUsername的查詢結果)
public Map<String , VerificFormMessage>checkSingupRequest(SignupRequest reqeuest , boolean isRegEmail , boolean isUsedUsername){
...
}
進入檢查後,會去檢查使用者輸入的資料,回傳個欄位檢驗的結果,來告知使用者填寫的資料,是否能順利註冊。而每個欄位都有自己的狀態,也新增三個Status類別,來儲存檢驗訊息。
分別是EmailStatus、PasswordStatus、UsernameStatus。每當都會經過自己檢查時,最後會把檢驗結果,放入到VerificFormMessage裡。若欄位狀態不符合200時,回到WebController時,會把錯誤訊息放入到flash裡,回到原本的註冊頁面,提示使用者。
WebController.java
path : /app/controllers/WebController.java
...
Map<String, VerificFormMessage> verificInfo = this.checkSingupRequest(request);
for (String key : verificInfo.keySet()) {
// 發現驗證沒過,放入錯誤訊息
if (!"200".equals(verificInfo.get(key).getStatus())) {
flash().put(key, verificInfo.get(key).getStatusDesc());
}
}
if(!flash().isEmpty()){
return ok(signup.render());
}
...
負責儲存驗證欄位的類別。
path : app/pojo/web/signup/verific/VerificFormMessage.java
package pojo.web.signup.verific;
/**
* 預設表單錯誤訊息
*/
public class VerificFormMessage {
// 錯誤的欄位名稱
private String inputName;
// 狀態碼
private String status;
// 狀態碼描述
private String statusDesc;
public String getInputName() {
return inputName;
}
public void setInputName(String inputName) {
this.inputName = inputName;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatusDesc() {
return statusDesc;
}
public void setStatusDesc(String statusDesc) {
this.statusDesc = statusDesc;
}
}
信箱檢查狀態。
path : app/pojo/web/signup/status/EmailStatus.java
package pojo.web.signup.status;
public enum EmailStatus {
S1("1", "請輸入信箱。"),
S2("2", "信箱格式錯誤。"),
S3("3", "該信箱已經註冊過,請更換信箱。"),
S200("200", "該信箱可以使用。");
EmailStatus(String status, String statusDesc) {
this.status = status;
this.statusDesc = statusDesc;
}
public final String status; // 狀態代碼
public final String statusDesc; // 狀態說明
}
密碼檢查狀態。
path : app/pojo/web/signup/status/PasswordStatus.java
package pojo.web.signup.status;
public enum PasswordStatus {
S1("1", "請輸入密碼。"),
S2("2", "請輸入確認密碼。"),
S3("3", "密碼兩次輸入不相符。"),
S4("4", "密碼需要在長度4~15個字元之間,且含有大小寫英文字與數字。"),
S200("200", "密碼可以使用。");
PasswordStatus(String status, String statusDesc) {
this.status = status;
this.statusDesc = statusDesc;
}
public final String status; // 狀態代碼
public final String statusDesc; // 狀態說明
}
使用者檢查狀態。
path : app/pojo/web/signup/status/UsernameStatus.java
package pojo.web.signup.status;
public enum UsernameStatus {
S1("1", "請輸入使用者名稱。"),
S2("2", "使用者名稱只能使用英文字,且介於4個字~15字之間。"),
S3("3", "該使用者名稱已被使用,請改用其它名稱。"),
S200("200", "使用者名稱可以使用。");
UsernameStatus(String status, String statusDesc) {
this.status = status;
this.statusDesc = statusDesc;
}
public final String status; // 狀態代碼
public final String statusDesc; // 狀態說明
}
WebService
path : /app/services/WebService.java
/** 檢查是否有該會員存在 */
public boolean checkMemberByEmail(String email);
/** 檢查是否有該會員使用者名稱是否存在 */
public boolean checkMemberByUsername(String username);
查詢的SQL語法。
path :/conf/services/WebService.xml
<!-- 檢查是否有重覆註冊的會員 -->
<select id="checkMemberByEmail" parameterType="String"
resultType="boolean">
SELECT count(1) FROM member_main WHERE email=#{email}
</select>
<!-- 檢查是否有重覆的使用者名稱 -->
<select id="checkMemberByUsername" parameterType="String"
resultType="boolean">
SELECT count(1) FROM member_main WHERE username=#{username}
</select>
檢查畫面。
Step 3 : 檢核通過,新增會員資料,且尚未認證。
通過檢查之後,代表使用者輸入的註冊資訊,在我們的資料庫,是沒有申請過的。這樣我們就可以新增一筆會員資料進去,且該新申請的會員狀態,是尚未認證的狀態。
特別要注意的地方就是,當有進行資料庫操作的動作時,都需要進行try catch的動作,以免跟資料庫互動時發生錯誤,導致頁面停擺無回應。
path : /app/controllers/WebController.java
...
try {
// Step 3
int isSignupOk = webService.signupNewMember(request);
if(isSignupOk == 0){
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
...
} catch (Exception e) {
e.printStackTrace();
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
呼叫WebService.signupNewMember新增會員資料。在MyBatis新增資料放入的資料是物件,我們需要在interface新增@Param的註記,並可以給予一個別名,在XML的SQL裡,就可以利用這個別名,取出每個欄位的資訊,寫入資料庫的member_main表單。
path : /app/services/WebService.java
public interface WebService {
/** 註冊新會員 */
public int signupNewMember(@Param("signupRequest") SignupRequest signupRequest);
...
}
新增資料的SQL語法。
email : 信箱
password : 密碼
username : 使用者名稱
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') => MySQL時間格式轉換,ex:20160924043200
path : /conf/services/WebService.xml
<!-- 註冊會員 -->
<insert id="signupNewMember" parameterType="pojo.web.signup.request.SignupRequest">
insert into
member_main (memberNo ,email , password , username , createDate, modifyDate)
values
(
null ,
#{signupRequest.email} ,
#{signupRequest.password},
#{signupRequest.username} ,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s') ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
);
</insert>
註冊失敗。
新增成功。
Step 4 : 註冊新增成功,新增認證連結資料。
會員註冊成功後,我們會先查詢出會員資料的信箱,組出認證信需要的認證字串,再寫入到member_token表單裡。
path : /app/controllers/WebController.java
...
try {
...
// Step 4
Utils_Signup utils_Signup = new Utils_Signup();
Member newMember = webService.findMemberByEmail(request.getEmail());
String signupAuthString = utils_Signup.genSignupAuthString(newMember.getEmail());
Map<String , String> memberToken = new HashMap<String , String>();
memberToken.put("memberNo", newMember.getMemberNo());
memberToken.put("tokenString", signupAuthString);
memberToken.put("type", MemberTokenType.Signup.toString());
int isSingAuthStringOk = webService.genSignupAuthData(memberToken);
...
}catch (Exception e) {
e.printStackTrace();
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
webService.findMemberByEmail,查詢會員資料,我們會需要新增一個Member類別,儲存查詢的會員資料。
path : /app/pojo/web/Member.java
package pojo.web;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Member {
private String memberNo;
private String email;
private String status;
private String password;
private String username;
private String createDate;
private String modifyDate;
public String getMemberNo() {
return memberNo;
}
public void setMemberNo(String memberNo) {
this.memberNo = memberNo;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getCreateDate() {
return createDate;
}
public void setCreateDate(String createDate) {
this.createDate = createDate;
}
public String getModifyDate() {
return modifyDate;
}
public void setModifyDate(String modifyDate) {
this.modifyDate = modifyDate;
}
}
認證字串使用會員的"電子信箱"與"目前時間"字串,串聯後,使用Sha256加密後產生。
path : /app/utils/signup/Utils_Signup.java
WebController.java
String authString = utils_Signup.genAuthString(newMember.getEmail());
-----
Utils_Signup.java
/**
* 產生註冊sha-256認證字串
* @param email 使用者信箱
* @return SHA256 String
*/
public String genSignupAuthString(String email){
Format formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String time = formatter.format(new Date());
String text = email + time;
return this.genSHA256String(text);
}
/**
* 傳入主要的需要認證字串,進行Sha256加密
* @param text
* @return token
*/
private String genSHA256String(String text){
String token = "";
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(text.getBytes("UTF-8"));
byte[] digest = md.digest();
// %064x 意思是,產生64個字串長的字串
token = String.format("%064x", new java.math.BigInteger(1, digest));
} catch (Exception e) {
e.printStackTrace();
}
return token;
}
...
member_token狀態表。
會員Token各種狀態表,預留忘記密碼與更換信箱。
package pojo.web;
/** 認證Token 類型*/
public enum MemberTokenType {
// 註冊
Signup ,
// 忘記密碼
ForgotPassword ,
// 更換信箱
ChangeEmail;
}
產生完字串後,新增一筆資料到member_token認證資料。
WebController.java
int isSingAuthStringOk = webService.genSignupAuthData(memberAuth);
----
WebService.java
/** 產生會員認證資料 */
public int genTokenData(@Param("memberToken") Map<String , String> memberToken);
/** 用Email 尋找會員資料 */
public Member findMemberByEmail(String email);
新增資料的SQL語法。
authString : 認證字串
memberNo : 會員編號
type : 類型
sendDate : 寄送日期
isUse : 是否使用過
createDate : 建立日期
modifyDate : 修改日期
expiryDate : 過期日期
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') => MySQL時間格式轉換,ex:20160924043200
path : /conf/services/WebService.xml
<!-- 註冊認證連結 -->
<insert id="genSignupAuthData" parameterType="Map">
insert into
member_token (tokenString, type ,memberNo , sendDate , isUse , createDate , modifyDate , expiryDate)
values
(
#{memberToken.tokenString} ,
#{memberToken.memberNo} ,
#{memberToken.type} ,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s'),
0,
DATE_FORMAT(NOW() , '%Y%m%d%H%i%s') ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') ,
DATE_FORMAT(DATE_ADD(NOW(),INTERVAL 1 DAY),'%Y%m%d%H%i%s')
);
</insert>
查詢的SQL語法。
<!-- 根據email查詢會員資料 -->
<select id="findMemberByEmail" parameterType="String"
resultType="pojo.web.Member">
SELECT * FROM member_main WHERE email = #{email}
</select>
寄送信件失敗。
新增成功。
Step 5 : 新增會員記錄檔。
會員加入成功後,我們需要新增一筆紀錄資料到member_login_log,紀錄使用者成功加入會員訊息。
path : /app/controllers/WebController.java
...
try {
...
// Step 5
Map<String , String> memberLoginData
= utils_Signup.genMemberLoginData(newMember.getMemberNo() ,
"PC" ,
request().remoteAddress() ,
MemberLoginStatus.S1.getStatus());
int isMemberLoginLogOk = webService.genMemberLoginLog(memberLoginData);
...
}catch (Exception e) {
e.printStackTrace();
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
會員紀錄檔,轉換成Map格式物件。
path : /app/utils/signup/Utils_Signup.java
WebController.java
Map<String , String> memberLoginData
= utils_Signup.genMemberLoginData(newMember.getMemberNo() ,
"PC" ,
request().remoteAddress() ,
MemberLoginStatus.S1.getStatus());
---
Utils_Signup.java
/**
* 產生會員記錄檔
* @param memberNo
* @param device
* @param ipAddress
* @param status
* @return Map<String , String>
*/
public Map<String , String> genMemberLoginData(String memberNo , String device , String ipAddress , String status){
Map<String , String> memberLoginLog = new HashMap<String , String>();
memberLoginLog.put("memberNo", memberNo);
memberLoginLog.put("device",device);
memberLoginLog.put("ipAddress",ipAddress);
memberLoginLog.put("status",status);
return memberLoginLog;
}
產生完Map物件後,新增一筆資料到member_login_log會員紀錄檔。
WebController.java
int isMemberLoginLogOk = webService.genMemberLoginLog(memberLoginData);
----
WebService.java
/** 會員記錄檔 */
public int genMemberLoginLog(@Param("memberLoginData") Map<String , String> memberLoginData);
新增資料的SQL語法。
memberNo : 會員編號
status : 會員狀態
ipAddress : 網路位址
device : 登入裝置
loginDate : 登入日期
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') => MySQL時間格式轉換,ex:20160924043200
path : /conf/services/WebService.xml
<!-- 會員記錄檔 -->
<insert id="genMemberLoginLog" parameterType="Map">
insert into
member_login_log (memberNo , status , ipAddress , device , loginDate)
values
(
#{memberLoginData.memberNo} ,
#{memberLoginData.status} ,
#{memberLoginData.ipAddress} ,
#{memberLoginData.device} ,
DATE_FORMAT(NOW(),'%Y%m%d%H%i%s')
);
</insert>
寄送信件失敗。
新增成功。
Step 6 : 進行寄送認證信動作。
最後進行寄送認證信動作。而寄信服務,需要有SMTP Server來寄送電子郵件,我採用Gmail的免費信箱,而Gmail有SMTP轉寄的服務,來協助我們寄送註冊信件。
path : /app/controllers/WebController.java
...
try {
...
// Step 6
Utils_Email utils_Email = new Utils_Email();
Email email = utils_Email.genSinupAuthEmail(newMember, authString);
boolean isSeadMailOk = utils_Email.sendMail(email);
...
}catch (Exception e) {
e.printStackTrace();
flash().put("signupError", "註冊會員失敗,請重新註冊,謝謝。");
return ok(signup.render());
}
Email格式大多是固定的,為了傳遞物件方便,新增Email類別,來設定一些Email常見的屬性。
path : /app/pojo/web/email/Email.java
package pojo.web.email;
public class Email {
// 寄件者
private String From;
// 收件者
private String to;
// 信件主題
private String subject;
// 文字訊息
private String text;
// 網頁訊息
private String content;
public String getFrom() {
return From;
}
public void setFrom(String from) {
From = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
新增Email寄送程式。
path : /app/utils/mail/Utils_Email.java
package utils.mail;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import play.libs.Json;
import pojo.web.Member;
import pojo.web.email.Email;
import services.Impl.WebServiceImpl;
import utils.signup.Utils_Signup;
public class Utils_Email {
public Email genSinupAuthEmail(Member member , String authString){
Email email = new Email();
email.setFrom("[email protected]");
email.setTo(member.getEmail());
email.setSubject("[STAR] - 註冊認證信");
email.setText("");
email.setContent("<h2>您好 "+ member.getUsername()+",你已經成功註冊,請在24小時內,點選以下驗證連結後,便會啟用帳號,謝謝!!</h2> "
+ "<a href='" +"http://127.0.0.1:9000/web/authMember?auth="+authString+"'>認證連結</a>");
play.Logger.info("auth email = " + Json.toJson(email));
return email;
}
// 寄信
public boolean sendMail(Email email) {
Session session = null;
try {
// 取得SMTP設定檔
Properties props = this.getMailSMTPConf();
// 進行授權認證動作
session = Session.getInstance(props, new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(props.getProperty("user"), props.getProperty("password"));
}
});
MimeMessage message = new MimeMessage(session);
String to = email.getTo();
message.setFrom(new InternetAddress(email.getFrom()));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject(email.getSubject());
if(!"".equals(email.getText())){
message.setText(email.getText());
}
if(!"".equals(email.getContent())){
message.setContent(email.getContent(), "text/html; charset=utf-8");
}
// 寄送訊息
Transport.send(message);
play.Logger.info("信件寄送成功");
return true;
} catch (MessagingException e) {
play.Logger.error("信件寄送失敗");
e.printStackTrace();
return false;
} finally {
// 成功寄信完,清除Session
session = null;
}
}
// 取得conf/application.conf設定檔
public Properties getMailSMTPConf(){
ClassLoader classLoader = utils.mail.Utils_Email.class.getClassLoader();
Config config = ConfigFactory.load(classLoader);
String host = config.getString("mail.smtp.host");
int port = config.getInt("mail.smtp.port");
final String user = config.getString("mail.smtp.user");
String password = config.getString("mail.smtp.password");
final boolean auth = config.getBoolean("mail.smtp.auth");
String ssl_trust = config.getString("mail.smtp.ssl.trust");
String socketFactory_class = config.getString("mail.smtp.socketFactory.class");
play.Logger.info("host = " + host);
play.Logger.info("port = " + port);
play.Logger.info("user = " + user);
play.Logger.info("password = " + password);
play.Logger.info("auth = " + auth);
play.Logger.info("ssl_trust = " + ssl_trust);
play.Logger.info("socketFactory_class = " + socketFactory_class);
Properties props = new Properties();
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
props.put("mail.smtp.socketFactory.port", port);
props.put("mail.smtp.socketFactory.class", socketFactory_class);
props.put("mail.smtp.auth", auth);
props.put("mail.smtp.ssl.trust", ssl_trust);
props.put("user", user);
props.put("password", password);
return props;
}
}
調整application.conf,新增SMTP相關設定。
path : conf/application.conf
...
# 設定我們的 mail smtp寄信服務
mail.smtp{
host="smtp.gmail.com"
port=465
ssl.trust="smtp.gmail.com"
socketFactory.class="javax.net.ssl.SSLSocketFactory"
auth="true"
user="[email protected]" <--這邊請更換成,您自己的Gmail信箱
password="xxx" <--這邊請更換成,您自己的Gmail信箱密碼
}
...
實際寄送註冊信件流程。
1.genSinupAuthEmail : 傳入Member會員資料,轉換成Email物件,依序設定主旨、收件者、信件主旨、信件內文。
2.sendMail : 傳入要寄信的Email物件,會依序先取得application.conf的SMTP設定值後,會跟Google進行帳號驗證後,再透過Gmail的SMTP,把我們的註冊信件送出。
WebController.java
Utils_Email utils_Email = new Utils_Email();
Email email = utils_Email.genSinupAuthEmail(newMember, authString);
boolean isSeadMailOk = utils_Email.sendMail(email);
---
Utils_Email.java
public Email genSinupAuthEmail(Member member , String authString) ...
-
public boolean sendMail(Email email) ...
Note : Google本身採用比較高的權限控管,若要使用SMTP的服務,先必須登入您的Google帳號,調整使用者的安全性調整為開啟,我們的寄信程式才能順利寄送信件。
網址 : https://www.google.com/settings/security/lesssecureapps
寄送失敗。
寄送成功。
Step 7 : 以上都順利完成,導入成功註冊頁面。
[Final]
這個小節,實際上花了快3個星期才完成,這期間調整前端的網頁畫面,以及後端的註冊資料的驗證與測試、寄送信件的測試。每個小節都幾乎是前面所有章節學到的技術統合,這邊相對需要花較多時間,才有辦法順利上手,往後幾個章節,才會相對輕鬆許多。若您完成之後,再前往下個小節,這部份講解會員認證信部份。
若有餘力,可以嘗試不同的註冊方式,可以練習,先用信箱申請註冊後,先傳送註冊連結,再往下申請帳號,這樣的優點是,若是經過這個步驟申請的帳號資料,會確保會員的電子信箱可以寄送到的,減少輸入錯誤信箱,而註冊失敗的案例。
[Reference]
Java Mail 使用GMail (SMTP) Server
http://pclevin.blogspot.tw/2014/11/java-mail-gmail-smtp-server.html
hash-string-via-sha-256-in-java
http://stackoverflow.com/questions/3103652/hash-string-via-sha-256-in-java