该过程仅供参考,若有优化与建议,欢迎提出。
目录
依赖
<!-- 发送邮件依赖 -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<!-- 谷歌缓存依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
邮箱验证
邮箱工具类
此处用的是线程的方式,Callable更容易设置是否超时
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.Callable;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class SEmail implements Callable<Boolean>{
private static String USERNAME = "";//邮箱地址XXX@XX.XX
/*
* POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
*需要给你的邮箱开启该功能,并填写相应信息
*/
private static String PASSWORD = "";//填写你的邮箱的授权码
private static String HOST = "";//选择一个可用的服务器
private String email;//目标邮箱
private String code;//生成的验证码
public SEmail(String email,String code){
this.email = email;
this.code = code;
}
public Boolean call() throws Exception{
Transport transport = null;
Properties prop = new Properties();
prop.setProperty("mail.smtp.host", HOST);
prop.setProperty("mai.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth","true");
Session session = Session.getDefaultInstance(prop);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(USERNAME));
message.setRecipient(Message.RecipientType.TO,new InternetAddress(email));
message.setSentDate(new Date());
message.setSubject("邮件验证码");
String info = "<!DOCTYPE html>\r\n" +
"<html lang=\"en\"" +
"<head>\r\n" +
" <meta charset=\"UTF-8\"/>\r\n" +
" <title>验证码</title>\r\n" +
"</head>\r\n" +
" <body>\r\n" +
" <h1>欢迎您使用XXX,这是您的验证码,请于X分钟内输入激活码</h1>\r\n" +
" <p>"+ code +"</p>"+
" </body>\r\n" +
"</html>";
message.setContent(info,"text/html;charset=UTF-8");
message.saveChanges();
session.setDebug(false);
transport = session.getTransport("smtp");
transport.connect(USERNAME,PASSWORD);
transport.sendMessage(message, message.getAllRecipients());
return true;
}
}
检查邮箱格式
public class CheckData {
private static String NUM = ".*\\d+.*";
private static String SMALL = ".*[a-zA-Z]+.*";
private static String CAPTIAL= ".*[a-zA-Z]+.*";
private static String SYMBOL = ".*([~!@#$%^&*()_+|<>,.?/:;'\\[\\]{\\\\}\\\"]){1,}+.*";
private static String EMAIL = "(\\w+([-.][A-Za-z0-9]+)*){3,18}@\\w+([-.][A-Za-z0-9]+)*\\.\\w+([-.][A-Za-z0-9]+)*";
/*
*邮箱格式检查
*/
public static boolean checkEmail(String email) {
if (email != null) {
return email.matches(EMAIL);
}
return false;
}
/*
*密码格式检查,此处是必须要有带小写+数字+不能有特殊符号
*/
public static boolean checkPass(String password) {
if (password == null){
return false;
}
int size = password.length();
if(size < 6||size >16) {
return false;
}
if(password.matches(SYMBOL)) {
return false;
}
int n = (password.matches(SMALL) ? 1 : 0) + (password.matches(NUM) ? 1 : 0) + (password.matches(CAPTIAL) ? 1 : 0);
return n==3;
}
}
Cache缓存
使用谷歌的一款包
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
public class VerifyCodeCache {
/**
* 5分钟的验证码缓存,保障线程安全
*/
private static volatile Cache<String,String> codeCache;
private VerifyCodeCache(){}
public static Cache<String,String> getCodeCache() {
//单例,懒汉模式
//双验证+同步锁
if (codeCache == null) {
synchronized (VerifyCodeCache.class) {
if (codeCache == null) {
codeCache = CacheBuilder.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.maximumSize(100)
.concurrencyLevel(50).build();
}
}
}
return codeCache;
}
}
SendEmail方法
/*
* 此处使用固定大小的线程池,限制可同时发送的数目
* 允许10个线程同时执行
* */
private ExecutorService executor = Executors.newFixedThreadPool(10);
/*
* 验证码缓存,单例
*/
private Cache<String,String> codeCache = VerifyCodeCache.getCodeCache();
@ResponseBody
@RequestMapping("")
public String sendEmail(String email){
...
if(!CheckData.checkEmail(email)) {
resMessage.setAll(StatusCode.send_fail,"邮箱错误");
...
}
StringBuilder builder = new StringBuilder();
Random random = new Random();
for(int i = 0 ; i<8;i++) {
builder.append(random.nextInt(9));
}
Future<Boolean> future = executor.submit(new SEmail(email,builder.toString()));
try {
//等待30秒,未执行完成,抛出中断异常
future.get(30, TimeUnit.SECONDS);
//放入对应邮箱以及验证码
codeCache.put(email, builder.toString());
...
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.getStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.getStackTrace();
} catch (TimeoutException e) {
// TODO Auto-generated catch block
e.getStackTrace();
}
...
}
token 生成
自定义对称加密
此处用的是Java自带的加密模式,以及自己的加密密钥
注意,UTF-8的byte与String 相互转换会不一样,因为无法表示,因此,使用的是ISO-8859-1
/*
*密钥
*/
private final static Key SECRET_KEY = new SecretKeySpec("密钥内容".getBytes(),"AES");
/*
* 生成key
* */
public static String createToken(String uuid) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
//获取当前时间
long currentTimeMillis = System.currentTimeMillis();
/*token中即为加密信息,可以自己随意设置
*此处为了方便,用的是uuid+有效时间
*TIME_OUT:为有时长,后通过比较,即可判断是否过期
*/
String token = uuid + ":"+(currentTimeMillis + TIME_OUT);
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, SECRET_KEY);
return new String(cipher.doFinal(token.getBytes()),"ISO-8859-1");
}
/*
* 还原
* */
public static String transferToken(String token) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException{
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, SECRET_KEY);
byte[] newData = cipher.doFinal(token.getBytes("ISO-8859-1"));
String res = new String(newData);
return res;
}
JWT 工具类
该类使用于快捷生成token而准备的,该类封装了所需要的方法。
可以参看JWT —— 生成Token、解析Token的简单工具类
跨域问题
可以参考-跨域问题详解
使用XMLHttpReuqest (xhr)时,经常遇到的问题。属于JS的一种保护措施。
发生原因
- 不同协议:即使是同一URL,当通过xhr访问却改变了协议时,也会被限制
- 不同域名:当前域名和请求域名不同。或者主域名相同,子域名不同(Cookie限制)。使用域名对应的其中一个IP与域名时.
- 不同端口:当前域名下不同端口
限制内容
- Cookie、LocalStorage和IndexDB无法获取。
- DOM元素无法获得。
- AJAX请求不能发送。
处理方式
发送JSONP请求替代XHR请求
JSONP请求的类型是JavaScript脚本(callback 作为前后端的约定,callback的值做为方法名,json内容作为方法的参数),而XHR请求的类型是json类型。
注意,AbstractJsonpResponseBodyAdvice 类基本在Sprig Boot中被废弃。
//ajax前端
$.ajax({
url: baseUrl + "/get1",
dataType: "jsonp", // 关键字段
jsonp: "callback", // 前后端默认的约定
cache: true, // 表示请求结果可以被缓存,url中不会有下划线参数了
success: function(json) {
result = json;
}
});
//Springboot服务端
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}
}
@CrossOrigin
它使用了一个额外的HTTP响应头来赋予当前user-agent(浏览器)获得在当前源(origin) 下使用非同源资源的权限 。(也就是加了一些信息,让它“看上去”同源)
使用方式:
直接在RequestMapping或者Controller注释的类/方法上添加该注释,Springboot就会完成相应的处理。
其有两个参数,若配置更细致,可以使用:
- Origins : 允许可访问的域列表
- maxAge:准备响应前的缓存持续的最大时间(以秒为单位)
其他方法
也有其他方法,比如服务器修改,反向代理等参看-跨域问题详解