Parcourir la source

Merge remote-tracking branch 'origin/master'

jingbo il y a 4 mois
Parent
commit
c1ae780b62

+ 13 - 1
pom.xml

@@ -252,7 +252,19 @@
 <!--			<artifactId>poi-ooxml</artifactId>-->
 <!--			<version>5.2.3</version>-->
 <!--		</dependency>-->
-
+		<!-- c3p0依赖 -->
+		<!--发送邮箱-->
+		<!-- JavaMail相关依赖 -->
+		<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>
 	

+ 25 - 0
src/main/java/com/jeesite/modules/mes/dao/MesEmailLoginCodeDao.java

@@ -0,0 +1,25 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesEmailLoginCode;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * MES email login verification code DAO.
+ */
+@MyBatisDao
+public interface MesEmailLoginCodeDao extends CrudDao<MesEmailLoginCode> {
+
+    MesEmailLoginCode findLatestUnused(@Param("loginCode") String loginCode, @Param("email") String email);
+
+    MesEmailLoginCode findLatest(@Param("loginCode") String loginCode, @Param("email") String email);
+
+    int countSentSince(@Param("email") String email, @Param("sendDate") String sendDate);
+
+    int countSentToday(@Param("email") String email, @Param("dayStart") String dayStart, @Param("dayEnd") String dayEnd);
+
+    int markUsed(@Param("id") String id);
+
+    int markExpired(@Param("id") String id);
+}

+ 103 - 0
src/main/java/com/jeesite/modules/mes/entity/MesEmailLoginCode.java

@@ -0,0 +1,103 @@
+package com.jeesite.modules.mes.entity;
+
+import java.util.Date;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+
+/**
+ * MES email login code.
+ */
+@Table(name = "mes_email_login_code", alias = "a", label = "MES邮箱登录验证码", columns = {
+        @Column(name = "id", attrName = "id", label = "主键", isPK = true),
+        @Column(name = "login_code", attrName = "loginCode", label = "登录账号", queryType = QueryType.EQ),
+        @Column(name = "email", attrName = "email", label = "绑定邮箱", queryType = QueryType.EQ),
+        @Column(name = "code", attrName = "code", label = "验证码"),
+        @Column(name = "send_date", attrName = "sendDate", label = "发送时间"),
+        @Column(name = "expire_date", attrName = "expireDate", label = "过期时间"),
+        @Column(name = "use_date", attrName = "useDate", label = "使用时间"),
+        @Column(name = "status", attrName = "status", label = "状态", comment = "0未使用 1已使用 2已过期"),
+        @Column(name = "create_by", attrName = "createBy", label = "创建者", isUpdate = false, isQuery = false),
+        @Column(name = "create_date", attrName = "createDate", label = "创建时间", isUpdate = false, isQuery = false),
+        @Column(name = "update_by", attrName = "updateBy", label = "更新者", isQuery = false),
+        @Column(name = "update_date", attrName = "updateDate", label = "更新时间", isQuery = false),
+        @Column(name = "remarks", attrName = "remarks", label = "备注", queryType = QueryType.LIKE),
+}, orderBy = "a.create_date DESC")
+public class MesEmailLoginCode extends DataEntity<MesEmailLoginCode> {
+
+    private static final long serialVersionUID = 1L;
+
+    private String loginCode;
+    private String email;
+    private String code;
+    private Date sendDate;
+    private Date expireDate;
+    private Date useDate;
+    private String status;
+
+    public MesEmailLoginCode() {
+        this(null);
+    }
+
+    public MesEmailLoginCode(String id) {
+        super(id);
+    }
+
+    public String getLoginCode() {
+        return loginCode;
+    }
+
+    public void setLoginCode(String loginCode) {
+        this.loginCode = loginCode;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public Date getSendDate() {
+        return sendDate;
+    }
+
+    public void setSendDate(Date sendDate) {
+        this.sendDate = sendDate;
+    }
+
+    public Date getExpireDate() {
+        return expireDate;
+    }
+
+    public void setExpireDate(Date expireDate) {
+        this.expireDate = expireDate;
+    }
+
+    public Date getUseDate() {
+        return useDate;
+    }
+
+    public void setUseDate(Date useDate) {
+        this.useDate = useDate;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+}

+ 183 - 0
src/main/java/com/jeesite/modules/mes/service/MesEmailLoginCodeService.java

@@ -0,0 +1,183 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.idgen.IdGen;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesEmailLoginCodeDao;
+import com.jeesite.modules.mes.entity.MesEmailLoginCode;
+import com.jeesite.modules.mes.util.MailUtil;
+import com.jeesite.modules.sys.entity.User;
+import com.jeesite.modules.sys.utils.UserUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Random;
+import java.util.regex.Pattern;
+
+/**
+ * MES email login verification code service.
+ */
+@Service
+public class MesEmailLoginCodeService extends CrudService<MesEmailLoginCodeDao, MesEmailLoginCode> {
+
+    public static final String LOGIN_SUCCESS = "SUCCESS";
+    public static final String LOGIN_ACCOUNT_PASSWORD_ERROR = "ACCOUNT_PASSWORD_ERROR";
+    public static final String LOGIN_EMAIL_MISMATCH = "EMAIL_MISMATCH";
+    public static final String LOGIN_CODE_EXPIRED = "CODE_EXPIRED";
+    public static final String LOGIN_CODE_ERROR = "CODE_ERROR";
+
+    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
+    private static final Pattern CODE_PATTERN = Pattern.compile("^\\d{6}$");
+    private static final int EXPIRE_MINUTES = 5;
+    private static final int SEND_INTERVAL_SECONDS = 60;
+    private static final int DAILY_LIMIT = 200;
+
+    @Autowired
+    private MesEmailLoginCodeDao mesEmailLoginCodeDao;
+
+    public boolean isEmailLoginEnabled() {
+        return "1".equals(Global.getConfig("mes.email.controller"));
+    }
+
+    public boolean isEmailFormat(String email) {
+        return StringUtils.isNotBlank(email) && EMAIL_PATTERN.matcher(email).matches();
+    }
+
+    public boolean isBoundEmail(User user, String email) {
+        return user != null && StringUtils.isNotBlank(user.getEmail()) && StringUtils.equalsIgnoreCase(StringUtils.trim(user.getEmail()), StringUtils.trim(email));
+    }
+
+    @Transactional
+    public String sendCode(String loginCode, String email) {
+        loginCode = StringUtils.trim(loginCode);
+        email = StringUtils.trim(email);
+        if (StringUtils.isBlank(loginCode)) {
+            return "请先填写登录账号。";
+        }
+        if (!isEmailFormat(email)) {
+            return "请填写正确的绑定邮箱。";
+        }
+        User user = UserUtils.getByLoginCode(loginCode);
+        if (user == null || !"0".equals(user.getStatus())) {
+            return "账号密码错误。";
+        }
+        if (StringUtils.isBlank(user.getEmail())) {
+            return "该账号未绑定邮箱,禁止登录。";
+        }
+        if (!isBoundEmail(user, email)) {
+            return "邮箱与账号不匹配。";
+        }
+
+        Date now = new Date();
+        if (mesEmailLoginCodeDao.countSentSince(email, format(addSeconds(now, -SEND_INTERVAL_SECONDS))) > 0) {
+            return "单个邮箱1分钟仅可发送1次验证码,请稍后再试。";
+        }
+        String dayStart = format(startOfDay(now));
+        String dayEnd = format(addDays(startOfDay(now), 1));
+        if (mesEmailLoginCodeDao.countSentToday(email, dayStart, dayEnd) >= DAILY_LIMIT) {
+            return "单个邮箱单日最多发送200次验证码。";
+        }
+
+        MesEmailLoginCode emailLoginCode = new MesEmailLoginCode();
+        emailLoginCode.setId(IdGen.randomBase62(19));
+        emailLoginCode.setIsNewRecord(true);
+        emailLoginCode.setLoginCode(loginCode);
+        emailLoginCode.setEmail(email);
+        emailLoginCode.setCode(randomCode());
+        emailLoginCode.setSendDate(now);
+        emailLoginCode.setExpireDate(addMinutes(now, EXPIRE_MINUTES));
+        emailLoginCode.setStatus("0");
+        save(emailLoginCode);
+
+        new Thread(new MailUtil(email, emailLoginCode.getCode())).start();
+        return null;
+    }
+
+    @Transactional
+    public String validateCode(String loginCode, String email, String code) {
+        loginCode = StringUtils.trim(loginCode);
+        email = StringUtils.trim(email);
+        code = StringUtils.trim(code);
+        User user = UserUtils.getByLoginCode(loginCode);
+        if (user == null || !"0".equals(user.getStatus())) {
+            return LOGIN_ACCOUNT_PASSWORD_ERROR;
+        }
+        if (StringUtils.isBlank(user.getEmail()) || !isBoundEmail(user, email)) {
+            return LOGIN_EMAIL_MISMATCH;
+        }
+        if (!CODE_PATTERN.matcher(code).matches()) {
+            return LOGIN_CODE_ERROR;
+        }
+
+        MesEmailLoginCode latest = mesEmailLoginCodeDao.findLatest(loginCode, email);
+        if (latest == null) {
+            return LOGIN_CODE_ERROR;
+        }
+        Date now = new Date();
+        if (latest.getExpireDate() == null || latest.getExpireDate().before(now)) {
+            if ("0".equals(latest.getStatus())) {
+                mesEmailLoginCodeDao.markExpired(latest.getId());
+            }
+            return LOGIN_CODE_EXPIRED;
+        }
+        if (!"0".equals(latest.getStatus())) {
+            return LOGIN_CODE_ERROR;
+        }
+        if (!StringUtils.equals(latest.getCode(), code)) {
+            return LOGIN_CODE_ERROR;
+        }
+        return LOGIN_SUCCESS;
+    }
+
+    @Transactional
+    public void markLatestCodeUsed(String loginCode, String email) {
+        MesEmailLoginCode latest = mesEmailLoginCodeDao.findLatestUnused(StringUtils.trim(loginCode), StringUtils.trim(email));
+        if (latest != null) {
+            mesEmailLoginCodeDao.markUsed(latest.getId());
+        }
+    }
+
+    private String randomCode() {
+        return String.format("%06d", new Random().nextInt(1000000));
+    }
+
+    private Date addMinutes(Date date, int minutes) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.MINUTE, minutes);
+        return calendar.getTime();
+    }
+
+    private Date addSeconds(Date date, int seconds) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.SECOND, seconds);
+        return calendar.getTime();
+    }
+
+    private Date addDays(Date date, int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.DATE, days);
+        return calendar.getTime();
+    }
+
+    private Date startOfDay(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTime();
+    }
+
+    private String format(Date date) {
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+    }
+}

+ 110 - 0
src/main/java/com/jeesite/modules/mes/util/MailUtil.java

@@ -0,0 +1,110 @@
+package com.jeesite.modules.mes.util;
+
+import com.sun.mail.util.MailSSLSocketFactory;
+
+import javax.mail.*;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.util.Properties;
+
+public class MailUtil implements Runnable {
+
+    private final String email;
+    private final String code;
+
+    public MailUtil(String email, String code) {
+        this.email = email;
+        this.code = code;
+    }
+
+    @Override
+    public void run() {
+        String from = "1273765445@qq.com";
+        String host = "smtp.qq.com";
+
+        Properties properties = System.getProperties();
+        properties.setProperty("mail.smtp.host", host);
+        properties.setProperty("mail.smtp.auth", "true");
+
+        try {
+            MailSSLSocketFactory sf = new MailSSLSocketFactory();
+            sf.setTrustAllHosts(true);
+            properties.put("mail.smtp.ssl.enable", "true");
+            properties.put("mail.smtp.ssl.socketFactory", sf);
+
+            Session session = Session.getDefaultInstance(properties, new Authenticator() {
+                @Override
+                public PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication("1273765445@qq.com", "avfwfzbbcqvphicb");
+                }
+            });
+
+            Message message = new MimeMessage(session);
+            message.setFrom(new InternetAddress(from));
+            message.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
+            message.setSubject("\u004d\u0045\u0053\u540e\u53f0\u767b\u5f55\u9a8c\u8bc1\u7801");
+            message.setContent(buildLoginCodeContent(email, code), "text/html;charset=UTF-8");
+
+            Transport.send(message);
+            System.out.println("Mail sent successfully.");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private String buildLoginCodeContent(String email, String code) {
+        String safeEmail = escapeHtml(email);
+        String safeCode = escapeHtml(code);
+        StringBuilder content = new StringBuilder();
+        content.append("<!doctype html>");
+        content.append("<html>");
+        content.append("<head>");
+        content.append("<meta charset=\"UTF-8\">");
+        content.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
+        content.append("</head>");
+        content.append("<body style=\"margin:0;padding:0;background:#f3f5f8;font-family:Arial,'Microsoft YaHei',sans-serif;color:#1f2937;\">");
+        content.append("<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#f3f5f8;padding:32px 12px;\">");
+        content.append("<tr><td align=\"center\">");
+        content.append("<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"max-width:560px;background:#ffffff;border-radius:18px;overflow:hidden;box-shadow:0 12px 36px rgba(15,23,42,.08);\">");
+        content.append("<tr><td style=\"padding:30px 34px 18px;background:#111827;\">");
+        content.append("<div style=\"width:44px;height:44px;line-height:44px;border-radius:12px;background:#2563eb;color:#fff;text-align:center;font-size:22px;font-weight:700;\">M</div>");
+        content.append("<div style=\"margin-top:28px;color:#93c5fd;font-size:16px;font-weight:700;\">\u004d\u0045\u0053 \u7ba1\u7406\u7cfb\u7edf</div>");
+        content.append("<h1 style=\"margin:10px 0 0;color:#ffffff;font-size:28px;line-height:1.35;font-weight:700;\">\u540e\u53f0\u767b\u5f55\u9a8c\u8bc1\u7801</h1>");
+        content.append("</td></tr>");
+        content.append("<tr><td style=\"padding:30px 34px 8px;\">");
+        content.append("<p style=\"margin:0;color:#4b5563;font-size:15px;line-height:1.8;\">\u60a8\u6b63\u5728\u4f7f\u7528\u90ae\u7bb1\u767b\u5f55\u004d\u0045\u0053\u540e\u53f0\uff0c\u8bf7\u5728\u767b\u5f55\u9875\u8f93\u5165\u4ee5\u4e0b\u9a8c\u8bc1\u7801\uff1a</p>");
+        content.append("<div style=\"margin:26px 0 24px;padding:26px 18px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:14px;text-align:center;\">");
+        content.append("<div style=\"color:#6b7280;font-size:13px;letter-spacing:.04em;text-transform:uppercase;\">\u767b\u5f55\u9a8c\u8bc1\u7801</div>");
+        content.append("<div style=\"margin-top:10px;color:#2563eb;font-size:42px;line-height:1;font-weight:800;letter-spacing:8px;\">");
+        content.append(safeCode);
+        content.append("</div>");
+        content.append("</div>");
+        content.append("<p style=\"margin:0;color:#4b5563;font-size:14px;line-height:1.8;\">\u8be5\u9a8c\u8bc1\u7801\u0035\u5206\u949f\u5185\u6709\u6548\uff0c\u4e14\u4ec5\u80fd\u4f7f\u7528\u4e00\u6b21\u3002\u5982\u975e\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u672c\u90ae\u4ef6\u5e76\u53ca\u65f6\u68c0\u67e5\u8d26\u53f7\u5b89\u5168\u3002</p>");
+        content.append("</td></tr>");
+        content.append("<tr><td style=\"padding:18px 34px 30px;\">");
+        content.append("<div style=\"padding:14px 16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;color:#9a3412;font-size:13px;line-height:1.7;\">");
+        content.append("\u5b89\u5168\u63d0\u9192\uff1a\u004d\u0045\u0053\u5de5\u4f5c\u4eba\u5458\u4e0d\u4f1a\u5411\u60a8\u7d22\u8981\u9a8c\u8bc1\u7801\uff0c\u8bf7\u52ff\u8f6c\u53d1\u6216\u5206\u4eab\u7ed9\u4ed6\u4eba\u3002");
+        content.append("</div>");
+        content.append("<p style=\"margin:18px 0 0;color:#9ca3af;font-size:12px;line-height:1.7;\">\u6536\u4ef6\u90ae\u7bb1\uff1a");
+        content.append(safeEmail);
+        content.append("<br>\u672c\u90ae\u4ef6\u7531\u7cfb\u7edf\u81ea\u52a8\u53d1\u9001\uff0c\u8bf7\u52ff\u76f4\u63a5\u56de\u590d\u3002</p>");
+        content.append("</td></tr>");
+        content.append("</table>");
+        content.append("</td></tr>");
+        content.append("</table>");
+        content.append("</body>");
+        content.append("</html>");
+        return content.toString();
+    }
+
+    private String escapeHtml(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#39;");
+    }
+}

+ 97 - 0
src/main/java/com/jeesite/modules/mes/web/MesEmailLoginController.java

@@ -0,0 +1,97 @@
+package com.jeesite.modules.mes.web;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.jeesite.common.codec.DesUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.shiro.filter.FormFilter;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.service.MesEmailLoginCodeService;
+import com.jeesite.modules.sys.utils.UserUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * MES email verification login controller.
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/sys/emailLogin")
+public class MesEmailLoginController extends BaseController {
+
+    @Autowired
+    private MesEmailLoginCodeService mesEmailLoginCodeService;
+
+    @RequestMapping(value = "sendCode")
+    @ResponseBody
+    public String sendCode(String username, String email) {
+        if (!mesEmailLoginCodeService.isEmailLoginEnabled()) {
+            return renderResult(Global.FALSE, "邮箱验证码登录未开启。");
+        }
+        String message = mesEmailLoginCodeService.sendCode(username, email);
+        if (StringUtils.isNotBlank(message)) {
+            return renderResult(Global.FALSE, message);
+        }
+        return renderResult(Global.TRUE, "验证码已发送,5分钟内有效。");
+    }
+
+    @RequestMapping(value = "login")
+    @ResponseBody
+    public void login(HttpServletRequest request, HttpServletResponse response) {
+        if (!mesEmailLoginCodeService.isEmailLoginEnabled()) {
+            FormFilter.onLoginFailure(loginException("邮箱验证码登录未开启。"), request, response);
+            return;
+        }
+        String secretKey = Global.getConfig("shiro.loginSubmit.secretKey");
+        String username = decode(request.getParameter("username"), secretKey);
+        String password = decode(request.getParameter("password"), secretKey);
+        String email = StringUtils.trim(request.getParameter("email"));
+        String emailCode = StringUtils.trim(request.getParameter("emailCode"));
+
+        String validateResult = mesEmailLoginCodeService.validateCode(username, email, emailCode);
+        if (!MesEmailLoginCodeService.LOGIN_SUCCESS.equals(validateResult)) {
+            FormFilter.onLoginFailure(loginException(messageOf(validateResult)), request, response);
+            return;
+        }
+
+        try {
+            UserUtils.getSubject().login(FormFilter.newToken(request, response));
+            mesEmailLoginCodeService.markLatestCodeUsed(username, email);
+            FormFilter.onLoginSuccess(request, response);
+        } catch (AuthenticationException e) {
+            FormFilter.onLoginFailure(loginException("账号密码错误。"), request, response);
+        }
+    }
+
+    private String decode(String value, String secretKey) {
+        if (StringUtils.isBlank(value) || StringUtils.isBlank(secretKey)) {
+            return value;
+        }
+        try {
+            return DesUtils.decode(value, secretKey);
+        } catch (Exception ex) {
+            return value;
+        }
+    }
+
+    private String messageOf(String validateResult) {
+        if (MesEmailLoginCodeService.LOGIN_EMAIL_MISMATCH.equals(validateResult)) {
+            return "邮箱与账号不匹配。";
+        }
+        if (MesEmailLoginCodeService.LOGIN_CODE_EXPIRED.equals(validateResult)) {
+            return "验证码过期。";
+        }
+        if (MesEmailLoginCodeService.LOGIN_CODE_ERROR.equals(validateResult)) {
+            return "验证码错误。";
+        }
+        return "账号密码错误。";
+    }
+
+    private AuthenticationException loginException(String message) {
+        return new AuthenticationException("msg:" + message);
+    }
+}

+ 114 - 0
src/main/java/com/jeesite/modules/test/entity/MailUtil.java

@@ -0,0 +1,114 @@
+package com.jeesite.modules.test.entity;
+
+import com.sun.mail.util.MailSSLSocketFactory;
+
+import javax.mail.Authenticator;
+import javax.mail.Message;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.util.Properties;
+
+public class MailUtil implements Runnable {
+
+    private final String email;
+    private final String code;
+
+    public MailUtil(String email, String code) {
+        this.email = email;
+        this.code = code;
+    }
+
+    @Override
+    public void run() {
+        String from = "1273765445@qq.com";
+        String host = "smtp.qq.com";
+
+        Properties properties = System.getProperties();
+        properties.setProperty("mail.smtp.host", host);
+        properties.setProperty("mail.smtp.auth", "true");
+
+        try {
+            MailSSLSocketFactory sf = new MailSSLSocketFactory();
+            sf.setTrustAllHosts(true);
+            properties.put("mail.smtp.ssl.enable", "true");
+            properties.put("mail.smtp.ssl.socketFactory", sf);
+
+            Session session = Session.getDefaultInstance(properties, new Authenticator() {
+                @Override
+                public PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication("1273765445@qq.com", "avfwfzbbcqvphicb");
+                }
+            });
+
+            Message message = new MimeMessage(session);
+            message.setFrom(new InternetAddress(from));
+            message.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
+            message.setSubject("\u004d\u0045\u0053\u540e\u53f0\u767b\u5f55\u9a8c\u8bc1\u7801");
+            message.setContent(buildLoginCodeContent(email, code), "text/html;charset=UTF-8");
+
+            Transport.send(message);
+            System.out.println("Mail sent successfully.");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private String buildLoginCodeContent(String email, String code) {
+        String safeEmail = escapeHtml(email);
+        String safeCode = escapeHtml(code);
+        StringBuilder content = new StringBuilder();
+        content.append("<!doctype html>");
+        content.append("<html>");
+        content.append("<head>");
+        content.append("<meta charset=\"UTF-8\">");
+        content.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
+        content.append("</head>");
+        content.append("<body style=\"margin:0;padding:0;background:#f3f5f8;font-family:Arial,'Microsoft YaHei',sans-serif;color:#1f2937;\">");
+        content.append("<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#f3f5f8;padding:32px 12px;\">");
+        content.append("<tr><td align=\"center\">");
+        content.append("<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"max-width:560px;background:#ffffff;border-radius:18px;overflow:hidden;box-shadow:0 12px 36px rgba(15,23,42,.08);\">");
+        content.append("<tr><td style=\"padding:30px 34px 18px;background:#111827;\">");
+        content.append("<div style=\"width:44px;height:44px;line-height:44px;border-radius:12px;background:#2563eb;color:#fff;text-align:center;font-size:22px;font-weight:700;\">M</div>");
+        content.append("<div style=\"margin-top:28px;color:#93c5fd;font-size:16px;font-weight:700;\">\u004d\u0045\u0053 \u7ba1\u7406\u7cfb\u7edf</div>");
+        content.append("<h1 style=\"margin:10px 0 0;color:#ffffff;font-size:28px;line-height:1.35;font-weight:700;\">\u540e\u53f0\u767b\u5f55\u9a8c\u8bc1\u7801</h1>");
+        content.append("</td></tr>");
+        content.append("<tr><td style=\"padding:30px 34px 8px;\">");
+        content.append("<p style=\"margin:0;color:#4b5563;font-size:15px;line-height:1.8;\">\u60a8\u6b63\u5728\u4f7f\u7528\u90ae\u7bb1\u767b\u5f55\u004d\u0045\u0053\u540e\u53f0\uff0c\u8bf7\u5728\u767b\u5f55\u9875\u8f93\u5165\u4ee5\u4e0b\u9a8c\u8bc1\u7801\uff1a</p>");
+        content.append("<div style=\"margin:26px 0 24px;padding:26px 18px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:14px;text-align:center;\">");
+        content.append("<div style=\"color:#6b7280;font-size:13px;letter-spacing:.04em;text-transform:uppercase;\">\u767b\u5f55\u9a8c\u8bc1\u7801</div>");
+        content.append("<div style=\"margin-top:10px;color:#2563eb;font-size:42px;line-height:1;font-weight:800;letter-spacing:8px;\">");
+        content.append(safeCode);
+        content.append("</div>");
+        content.append("</div>");
+        content.append("<p style=\"margin:0;color:#4b5563;font-size:14px;line-height:1.8;\">\u8be5\u9a8c\u8bc1\u7801\u0035\u5206\u949f\u5185\u6709\u6548\uff0c\u4e14\u4ec5\u80fd\u4f7f\u7528\u4e00\u6b21\u3002\u5982\u975e\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u672c\u90ae\u4ef6\u5e76\u53ca\u65f6\u68c0\u67e5\u8d26\u53f7\u5b89\u5168\u3002</p>");
+        content.append("</td></tr>");
+        content.append("<tr><td style=\"padding:18px 34px 30px;\">");
+        content.append("<div style=\"padding:14px 16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;color:#9a3412;font-size:13px;line-height:1.7;\">");
+        content.append("\u5b89\u5168\u63d0\u9192\uff1a\u004d\u0045\u0053\u5de5\u4f5c\u4eba\u5458\u4e0d\u4f1a\u5411\u60a8\u7d22\u8981\u9a8c\u8bc1\u7801\uff0c\u8bf7\u52ff\u8f6c\u53d1\u6216\u5206\u4eab\u7ed9\u4ed6\u4eba\u3002");
+        content.append("</div>");
+        content.append("<p style=\"margin:18px 0 0;color:#9ca3af;font-size:12px;line-height:1.7;\">\u6536\u4ef6\u90ae\u7bb1\uff1a");
+        content.append(safeEmail);
+        content.append("<br>\u672c\u90ae\u4ef6\u7531\u7cfb\u7edf\u81ea\u52a8\u53d1\u9001\uff0c\u8bf7\u52ff\u76f4\u63a5\u56de\u590d\u3002</p>");
+        content.append("</td></tr>");
+        content.append("</table>");
+        content.append("</td></tr>");
+        content.append("</table>");
+        content.append("</body>");
+        content.append("</html>");
+        return content.toString();
+    }
+
+    private String escapeHtml(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#39;");
+    }
+}

+ 34 - 0
src/main/java/com/jeesite/modules/test/web/Demodingding.java

@@ -0,0 +1,34 @@
+package com.jeesite.modules.test.web;
+
+import com.jeesite.modules.test.entity.MailUtil;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * TODO
+ *
+ * @Author huang
+ * @Date 2026/6/8 2026/6/8
+ */
+@SpringBootTest
+public class Demodingding {
+    public static void main(String[] args) {
+        //生成激活码
+        String code="ABCD";
+        //保存成功则通过线程的方式给用户发送一封邮件
+        new Thread(new MailUtil("15518269125@163.com", code)).start();;
+    }
+    public boolean doRegister(String userName, String password, String email) {
+        // 这里可以验证各字段是否为空
+
+        //利用正则表达式(可改进)验证邮箱是否符合邮箱的格式
+        if(!email.matches("^\\w+@(\\w+\\.)+\\w+$")){
+            return false;
+        }
+        //生成激活码
+        String code="ABCD";
+        //保存成功则通过线程的方式给用户发送一封邮件
+            new Thread(new MailUtil("1273765445@qq.com", code)).start();;
+            return true;
+    }
+
+}

+ 2 - 0
src/main/resources/config/application.yml

@@ -617,6 +617,8 @@ shiro:
   filterChainDefinitions: |
     ${adminPath}/mes/mesApp/ver = anon
     ${adminPath}/mes/mesLogin/login = anon
+    ${adminPath}/sys/emailLogin/sendCode = anon
+    ${adminPath}/sys/emailLogin/login = anon
     ${adminPath}/mes/mesProductRecord/ghtime = anon
     ${adminPath}/mes/mesProductRecord/qmcheck = anon
     ${adminPath}/mes/mesProductRecord/qmresult = anon

+ 85 - 0
src/main/resources/mappings/modules/mes/MesEmailLoginCodeDao.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesEmailLoginCodeDao">
+
+    <select id="findLatestUnused" resultType="com.jeesite.modules.mes.entity.MesEmailLoginCode">
+        SELECT
+            id,
+            login_code AS loginCode,
+            email,
+            code,
+            send_date AS sendDate,
+            expire_date AS expireDate,
+            use_date AS useDate,
+            status,
+            create_by AS createBy,
+            create_date AS createDate,
+            update_by AS updateBy,
+            update_date AS updateDate,
+            remarks
+        FROM
+            mes_email_login_code a
+        WHERE
+            a.login_code = #{loginCode}
+            AND a.email = #{email}
+            AND a.status = '0'
+        ORDER BY a.send_date DESC
+        LIMIT 1
+    </select>
+
+    <select id="findLatest" resultType="com.jeesite.modules.mes.entity.MesEmailLoginCode">
+        SELECT
+            id,
+            login_code AS loginCode,
+            email,
+            code,
+            send_date AS sendDate,
+            expire_date AS expireDate,
+            use_date AS useDate,
+            status,
+            create_by AS createBy,
+            create_date AS createDate,
+            update_by AS updateBy,
+            update_date AS updateDate,
+            remarks
+        FROM
+            mes_email_login_code a
+        WHERE
+            a.login_code = #{loginCode}
+            AND a.email = #{email}
+        ORDER BY a.send_date DESC
+        LIMIT 1
+    </select>
+
+    <select id="countSentSince" resultType="int">
+        SELECT COUNT(1)
+        FROM mes_email_login_code
+        WHERE email = #{email}
+          AND send_date >= #{sendDate}
+    </select>
+
+    <select id="countSentToday" resultType="int">
+        SELECT COUNT(1)
+        FROM mes_email_login_code
+        WHERE email = #{email}
+          AND send_date >= #{dayStart}
+          AND send_date &lt; #{dayEnd}
+    </select>
+
+    <update id="markUsed">
+        UPDATE mes_email_login_code
+        SET status = '1',
+            use_date = NOW(),
+            update_date = NOW()
+        WHERE id = #{id}
+          AND status = '0'
+    </update>
+
+    <update id="markExpired">
+        UPDATE mes_email_login_code
+        SET status = '2',
+            update_date = NOW()
+        WHERE id = #{id}
+          AND status = '0'
+    </update>
+</mapper>

+ 29 - 0
src/main/resources/static/modules/sys/sysLogin.css

@@ -100,6 +100,35 @@
     margin-top: 8px;
 }
 
+.login-box-body .email-code-group {
+    width: 100%;
+}
+
+.login-box-body .email-code-group .form-control {
+    border-radius: 6px 0 0 6px;
+}
+
+.login-box-body .email-code-group .form-control-feedback {
+    left: 0;
+    right: auto;
+    z-index: 4;
+}
+
+.login-box-body .email-code-group .btn {
+    height: 44px;
+    min-width: 108px;
+    border-radius: 0 6px 6px 0;
+    border-color: #1f66d1;
+    background: #256bd6;
+    color: #fff;
+}
+
+.login-box-body .email-code-group .btn[disabled] {
+    border-color: #c8d1df;
+    background: #d8e1ee;
+    color: #7c8798;
+}
+
 .login-copyright {
     position: absolute;
     bottom: 20px;

+ 117 - 0
src/main/resources/static/modules/sys/sysLogin.js

@@ -0,0 +1,117 @@
+try {
+    if (window.toTopWindow !== undefined && window.toTopWindow === true && self.frameElement && self.frameElement.tagName === 'IFRAME') {
+        js.window.location = ctx + '/login';
+    }
+} catch (e) {
+}
+
+$(function () {
+    var waitSeconds = 60;
+
+    function isEmail(value) {
+        return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test($.trim(value || ''));
+    }
+
+    function refreshEmailCodeButton() {
+        var button = $('#sendEmailLoginCode');
+        if (button.length === 0 || button.data('counting') === true) {
+            return;
+        }
+        button.prop('disabled', !isEmail($('#email').val()));
+    }
+
+    function countDown(button) {
+        $(button).data('counting', true);
+        if (waitSeconds === 0) {
+            button.removeAttribute('disabled');
+            button.value = '获取验证码';
+            waitSeconds = 60;
+            $(button).data('counting', false);
+            refreshEmailCodeButton();
+        } else {
+            button.setAttribute('disabled', true);
+            button.value = '重新发送(' + waitSeconds + ')';
+            waitSeconds--;
+            setTimeout(function () {
+                countDown(button);
+            }, 1000);
+        }
+    }
+
+    $('#username, #password, #email, #emailCode').on('focus blur', function () {
+        var input = this;
+        setTimeout(function () {
+            var borderColor = $(input).css('borderColor');
+            if (borderColor !== '') {
+                $(input).prev().css('color', borderColor);
+            }
+        }, 100);
+    }).blur();
+
+    $('#email').on('input blur', refreshEmailCodeButton);
+    refreshEmailCodeButton();
+
+    $('#sendEmailLoginCode').click(function () {
+        var username = $('#username').val();
+        if (!username) {
+            js.showMessage('请先填写登录账号。');
+            $('#username').focus();
+            return;
+        }
+        var email = $('#email').val();
+        if (!isEmail(email)) {
+            js.showMessage('请填写正确的绑定邮箱。');
+            $('#email').focus();
+            return;
+        }
+        var button = this;
+        js.ajaxSubmit(ctxAdmin + '/sys/emailLogin/sendCode', {username: username, email: email}, function (data) {
+            js.showMessage(data.message);
+            if (data.result === 'true') {
+                countDown(button);
+                $('#emailCode').focus();
+            }
+        });
+    });
+
+    $('#loginForm').validate({
+        ignore: ':hidden',
+        submitHandler: function (form) {
+            var formEl = $(form);
+            var action = formEl.attr('action');
+            var data = formEl.serializeArray();
+            var secretKey = window.secretKey || $('#loginKey').data('key');
+            if (secretKey !== '') {
+                for (var i = 0, len = data.length; i < len; i++) {
+                    if (data[i].name === 'username') {
+                        data[i].value = DesUtils.encode($('#username').val(), secretKey);
+                    } else if (data[i].name === 'password') {
+                        data[i].value = DesUtils.encode($('#password').val(), secretKey);
+                    } else if (data[i].name === 'validCode') {
+                        data[i].value = DesUtils.encode($('#validCode').val(), secretKey);
+                    }
+                }
+            }
+            js.ajaxSubmit(action, data, function (data) {
+                if (data.isValidCodeLogin === 1) {
+                    $('#isValidCodeLogin').show();
+                    $('#validCodeRefresh').click();
+                }
+                if (data.result === 'false') {
+                    if (data.message && data.message.length > 0) {
+                        js.showMessage(data.message);
+                    }
+                } else {
+                    js.loading($('#btnSubmit').data('loading'));
+                    location = data.__url && data.__url !== '' ? data.__url : ctxAdmin + '/index';
+                }
+            }, 'json', true, $('#btnSubmit').data('loginValid'));
+            $('#password').select().focus();
+        }
+    });
+
+    var url = $('#loginForm [name=__url]');
+    if (url.length > 0 && url.val() !== '') {
+        url.val(url.val() + window.location.hash);
+    }
+});

+ 24 - 3
src/main/resources/views/themes/default/modules/sys/sysLogin.html

@@ -18,16 +18,17 @@
 <!--			<a class="switchSkin" href="${ctxAdmin}/switchSkin/skin-dark" title="${text('黑暗模式')}"><i class="fa fa-toggle-on"></i></a>-->
 		<% } %>
 		<% var isLoginByValidCode = @Global.getConfigToBoolean('user.loginByValidCode', 'true'); %>
+		<% var isEmailLoginEnabled = @Global.getConfig('mes.email.controller', '0') == '1'; %>
 		<% if(isLoginByValidCode){ %>
 			<ul id="loginTab" class="nav nav-tabs">
-				<li class="active"><a href="#tab-1" data-toggle="tab" action="${ctxAdmin}/login">${text('账号登录')}</a></li>
+				<li class="active"><a href="#tab-1" data-toggle="tab" action="${isEmailLoginEnabled ? ctxAdmin + '/sys/emailLogin/login' : ctxAdmin + '/login'}">${text('账号登录')}</a></li>
 <!--				<li><a href="#tab-2" data-toggle="tab" action="${ctxPath}/account/loginByValidCode">${text('手机登录')}</a></li>-->
 				<% if(isNotBlank(@Global.getConfig('shiro.ldapUrl'))){ %>
 <!--					<li><a href="#tab-3" data-toggle="tab" action="${ctxAdmin}/login-ldap">${text('LDAP登录')}</a></li>-->
 				<% } %>
 			</ul>
 		<% } %>
-		<#form:form id="loginForm" model="${user!}" action="${ctxAdmin}/login" method="post" class="tab-content">
+		<#form:form id="loginForm" model="${user!}" action="${isEmailLoginEnabled ? ctxAdmin + '/sys/emailLogin/login' : ctxAdmin + '/login'}" method="post" class="tab-content">
 			<% if(!isLoginByValidCode && isBlank(message!)){ %>
 				<h4 class="login-box-msg">${text('欢迎回来')+'!'}</h4>
 			<% }else if(isNotBlank(message!)){ %>
@@ -47,6 +48,26 @@
 				<#form:input type="password" name="password" class="form-control required"
 					data-msg-required="${text('请填写登录密码.')}" placeholder="${text('登录密码')}" autocomplete="off"/>
 			</div>
+			<% if(isEmailLoginEnabled){ %>
+			<div class="form-group has-feedback tab-pane tab-1 active email-login-row">
+				<span class="icon-envelope form-control-feedback" title="${text('绑定邮箱')}"></span>
+				<#form:input type="text" name="email" id="email" class="form-control required email"
+					data-msg-required="${text('请填写绑定邮箱')}" data-msg-email="${text('请填写正确的邮箱地址')}"
+					placeholder="${text('绑定邮箱')}" autocomplete="off"/>
+			</div>
+			<div class="form-group has-feedback tab-pane tab-1 active email-login-row">
+				<div class="input-group email-code-group">
+					<span class="icon-shield form-control-feedback" title="${text('邮箱验证码')}"></span>
+					<input type="text" id="emailCode" name="emailCode" maxlength="6"
+						class="form-control required digits" data-msg-required="${text('请填写邮箱验证码')}"
+						data-msg-digits="${text('邮箱验证码为6位数字')}" data-rule-rangelength="6,6"
+						data-msg-rangelength="${text('邮箱验证码为6位数字')}" placeholder="${text('邮箱验证码')}" autocomplete="off" />
+					<span class="input-group-btn">
+						<input type="button" id="sendEmailLoginCode" value="${text('获取验证码')}" class="btn btn-flat" disabled="disabled"/>
+					</span>
+				</div>
+			</div>
+			<% } %>
 			<% if(@Global.getConfigToBoolean('user.loginCodeCorpUnique', 'false')){ %>
 			<div class="form-group has-feedback">
 				<#form:treeselect id="switchCorpSelect" title="${text('登录租户')}" allowClear="false"
@@ -116,4 +137,4 @@
 <% } %>
 <script src="${ctxStatic}/common/des.js?${_version}"></script>
 <script src="${ctxStatic}/jquery-toastr/2.1/toastr.min.js?${_version}"></script>
-<script src="${ctxStatic}/modules/sys/sysLogin.js?${_version}"></script>
+<script src="${ctxStatic}/modules/sys/sysLogin.js?${_version}"></script>