Java secret - 03

文章目录

  • Java secret - 03
    • 源管理工具类
    • 全局邮件帐户
    • 邮件内部工具类
    • 邮件发送客户端
    • 邮件账户对象
    • 邮件异常工具类
    • 邮件工具类
    • 邮件用户名密码验证器
    • Redis使用FastJson序列化
    • redis配置
    • 缓存 通用常量
    • spring redis 工具类
    • Token解析器
    • Redis授权认证器
    • Redis授权
    • 文件上传工具类

Java secret - 03

源管理工具类

package com.xueyi.common.datasource.utils;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.xueyi.common.cache.utils.SourceUtil;
import com.xueyi.common.core.exception.ServiceException;
import com.xueyi.common.core.exception.UtilException;
import com.xueyi.common.core.utils.core.CollUtil;
import com.xueyi.common.core.utils.core.ObjectUtil;
import com.xueyi.common.core.utils.core.SpringUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import com.xueyi.tenant.api.source.domain.dto.TeSourceDto;
import lombok.extern.slf4j.Slf4j;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 源管理工具类
 *
 * @author xueyi
 */
@Slf4j
public class DSUtil {

    /**
     * 数据源动态加载
     *
     * @param sourceName 数据源编码
     */
    public static String loadDs(String sourceName) {
        if (StrUtil.isEmpty(sourceName)) {
            throw new UtilException("数据源不存在!");
        } else if (checkHasDs(sourceName)) {
            return sourceName;
        }
        TeSourceDto source = SourceUtil.getTeSourceCache(sourceName);
        if (ObjectUtil.isNull(source)) {
            throw new UtilException("数据源缓存不存在!");
        }
        addDs(source);
        return sourceName;
    }

    /**
     * 添加一个数据源到数据源库中
     *
     * @param source 数据源对象
     */
    public static void addDs(TeSourceDto source) {
        try {
            DefaultDataSourceCreator dataSourceCreator = SpringUtil.getBean(DefaultDataSourceCreator.class);
            DataSourceProperty dataSourceProperty = new DataSourceProperty();
            dataSourceProperty.setDriverClassName(source.getDriverClassName());
            dataSourceProperty.setUrl(source.getUrlPrepend() + source.getUrlAppend());
            dataSourceProperty.setUsername(source.getUserName());
            dataSourceProperty.setPassword(source.getPassword());
            DataSource dataSource = SpringUtil.getBean(DataSource.class);
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
            dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
            ds.addDataSource(source.getSlave(), dataSource);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new UtilException("数据源添加失败!");
        }
    }

    /**
     * 从数据源库中删除一个数据源
     *
     * @param slave 数据源编码
     */
    public static void delDs(String slave) {
        try {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) SpringUtil.getBean(DataSource.class);
            ds.removeDataSource(slave);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new UtilException("数据源删除失败!");
        }
    }

    /**
     * 获取当前数据源库中所有数据源
     */
    public static void getDs() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) SpringUtil.getBean(DataSource.class);
        ds.getDataSources().keySet().forEach(System.out::println);
    }

    /**
     * 是否存在指定数据源
     */
    public static boolean checkHasDs(String slave) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) SpringUtil.getBean(DataSource.class);
        return ds.getDataSources().containsKey(slave);
    }

    /**
     * 获取当前线程数据源名称
     */
    public static String getNowDsName() {
        return DynamicDataSourceContextHolder.peek();
    }

    /**
     * 测试数据源是否可连接
     *
     * @param source 数据源对象
     */
    public static void testDs(TeSourceDto source) {
        try {
            Class.forName(source.getDriverClassName());
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new UtilException("数据源驱动加载失败,请检查驱动信息!");
        }
        try {
            Connection dbConn = DriverManager.getConnection(source.getUrlPrepend() + source.getUrlAppend(), source.getUserName(), source.getPassword());
            dbConn.close();
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new UtilException("数据源连接失败,请检查连接信息!");
        }
    }

    /**
     * 测试数据源是否为可连接子库
     *
     * @param source 数据源对象
     */
    public static void testSlaveDs(TeSourceDto source, List<String> needTable) {
        try {
            Class.forName(source.getDriverClassName());
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new ServiceException("数据源驱动加载失败");
        }
        try {
            Connection dbConn = DriverManager.getConnection(source.getUrlPrepend() + source.getUrlAppend(), source.getUserName(), source.getPassword());
            PreparedStatement statement = dbConn.prepareStatement("select table_name from information_schema.tables where table_schema = (select database())");
            ResultSet resultSet = statement.executeQuery();
            Set<String> tableNameList = new HashSet<>();
            while (resultSet.next()) {
                tableNameList.add(resultSet.getString("table_name"));
            }
            Set<String> slaveTables = new HashSet<>(needTable);
            slaveTables.removeAll(tableNameList);
            if (CollUtil.isNotEmpty(slaveTables)) {
                throw new ServiceException("请连接包含子库数据表信息的数据源!");
            }
            dbConn.close();
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new ServiceException("数据源连接失败,请检查连接信息!");
        }
    }
}

全局邮件帐户

package com.xueyi.common.mail.utils;

import cn.hutool.core.io.IORuntimeException;

/**
 * 全局邮件帐户
 *
 * @author kevin
 */
public enum GlobalMailAccount {
    INSTANCE;

    private final MailAccount mailAccount;

    /**
     * 构造
     */
    GlobalMailAccount() {
        mailAccount = createDefaultAccount();
    }

    /**
     * 获得邮件帐户
     *
     * @return 邮件帐户
     */
    public MailAccount getAccount() {
        return this.mailAccount;
    }

    /**
     * 创建默认帐户
     *
     * @return MailAccount
     */
    private MailAccount createDefaultAccount() {
        for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
            try {
                return new MailAccount(mailSettingPath);
            } catch (IORuntimeException ignore) {
                //ignore
            }
        }
        return null;
    }
}

邮件内部工具类

package com.xueyi.common.mail.utils;

import cn.hutool.core.util.ArrayUtil;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 邮件内部工具类
 *
 * @author kevin
 */
public class InternalMailUtil {

    /**
     * 将多个字符串邮件地址转为{@link InternetAddress}列表
* 单个字符串地址可以是多个地址合并的字符串 * * @param addrStrs 地址数组 * @param charset 编码(主要用于中文用户名的编码) * @return 地址数组 * @since 4.0.3 */
public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) { final List<InternetAddress> resultList = new ArrayList<>(addrStrs.length); InternetAddress[] addrs; for (String addrStr : addrStrs) { addrs = parseAddress(addrStr, charset); if (ArrayUtil.isNotEmpty(addrs)) { Collections.addAll(resultList, addrs); } } return resultList.toArray(new InternetAddress[0]); } /** * 解析第一个地址 * * @param address 地址字符串 * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码 * @return 地址列表 */ public static InternetAddress parseFirstAddress(String address, Charset charset) { final InternetAddress[] internetAddresses = parseAddress(address, charset); if (ArrayUtil.isEmpty(internetAddresses)) { try { return new InternetAddress(address); } catch (AddressException e) { throw new MailException(e); } } return internetAddresses[0]; } /** * 将一个地址字符串解析为多个地址
* 地址间使用" "、","、";"分隔 * * @param address 地址字符串 * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码 * @return 地址列表 */
public static InternetAddress[] parseAddress(String address, Charset charset) { InternetAddress[] addresses; try { addresses = InternetAddress.parse(address); } catch (AddressException e) { throw new MailException(e); } //编码用户名 if (ArrayUtil.isNotEmpty(addresses)) { final String charsetStr = null == charset ? null : charset.name(); for (InternetAddress internetAddress : addresses) { try { internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr); } catch (UnsupportedEncodingException e) { throw new MailException(e); } } } return addresses; } /** * 编码中文字符
* 编码失败返回原字符串 * * @param text 被编码的文本 * @param charset 编码 * @return 编码后的结果 */
public static String encodeText(String text, Charset charset) { try { return MimeUtility.encodeText(text, charset.name(), null); } catch (UnsupportedEncodingException e) { // ignore } return text; } }

邮件发送客户端

package com.xueyi.common.mail.utils;

import cn.hutool.core.builder.Builder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.mail.Address;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.SendFailedException;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import jakarta.mail.util.ByteArrayDataSource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Serial;
import java.nio.charset.Charset;
import java.util.Date;

/**
 * 邮件发送客户端
 *
 * @author kevin
 */
public class Mail implements Builder<MimeMessage> {
    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 邮箱帐户信息以及一些客户端配置信息
     */
    private final MailAccount mailAccount;
    /**
     * 收件人列表
     */
    private String[] tos;
    /**
     * 抄送人列表(carbon copy)
     */
    private String[] ccs;
    /**
     * 密送人列表(blind carbon copy)
     */
    private String[] bccs;
    /**
     * 回复地址(reply-to)
     */
    private String[] reply;
    /**
     * 标题
     */
    private String title;
    /**
     * 内容
     */
    private String content;
    /**
     * 是否为HTML
     */
    private boolean isHtml;
    /**
     * 正文、附件和图片的混合部分
     */
    private final Multipart multipart = new MimeMultipart();
    /**
     * 是否使用全局会话,默认为false
     */
    private boolean useGlobalSession = false;

    /**
     * debug输出位置,可以自定义debug日志
     */
    private PrintStream debugOutput;

    /**
     * 创建邮件客户端
     *
     * @param mailAccount 邮件帐号
     * @return Mail
     */
    public static Mail create(MailAccount mailAccount) {
        return new Mail(mailAccount);
    }

    /**
     * 创建邮件客户端,使用全局邮件帐户
     *
     * @return Mail
     */
    public static Mail create() {
        return new Mail();
    }

    // --------------------------------------------------------------- Constructor start

    /**
     * 构造,使用全局邮件帐户
     */
    public Mail() {
        this(GlobalMailAccount.INSTANCE.getAccount());
    }

    /**
     * 构造
     *
     * @param mailAccount 邮件帐户,如果为null使用默认配置文件的全局邮件配置
     */
    public Mail(MailAccount mailAccount) {
        mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
        this.mailAccount = mailAccount.defaultIfEmpty();
    }
    // --------------------------------------------------------------- Constructor end

    // --------------------------------------------------------------- Getters and Setters start

    /**
     * 设置收件人
     *
     * @param tos 收件人列表
     * @return this
     * @see #setTos(String...)
     */
    public Mail to(String... tos) {
        return setTos(tos);
    }

    /**
     * 设置多个收件人
     *
     * @param tos 收件人列表
     * @return this
     */
    public Mail setTos(String... tos) {
        this.tos = tos;
        return this;
    }

    /**
     * 设置多个抄送人(carbon copy)
     *
     * @param ccs 抄送人列表
     * @return this
     * @since 4.0.3
     */
    public Mail setCcs(String... ccs) {
        this.ccs = ccs;
        return this;
    }

    /**
     * 设置多个密送人(blind carbon copy)
     *
     * @param bccs 密送人列表
     * @return this
     * @since 4.0.3
     */
    public Mail setBccs(String... bccs) {
        this.bccs = bccs;
        return this;
    }

    /**
     * 设置多个回复地址(reply-to)
     *
     * @param reply 回复地址(reply-to)列表
     * @return this
     * @since 4.6.0
     */
    public Mail setReply(String... reply) {
        this.reply = reply;
        return this;
    }

    /**
     * 设置标题
     *
     * @param title 标题
     * @return this
     */
    public Mail setTitle(String title) {
        this.title = title;
        return this;
    }

    /**
     * 设置正文
* 正文可以是普通文本也可以是HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} 设置是否为HTML * * @param content 正文 * @return this */
public Mail setContent(String content) { this.content = content; return this; } /** * 设置是否是HTML * * @param isHtml 是否为HTML * @return this */ public Mail setHtml(boolean isHtml) { this.isHtml = isHtml; return this; } /** * 设置正文 * * @param content 正文内容 * @param isHtml 是否为HTML * @return this */ public Mail setContent(String content, boolean isHtml) { setContent(content); return setHtml(isHtml); } /** * 设置文件类型附件,文件可以是图片文件,此时自动设置cid(正文中引用图片),默认cid为文件名 * * @param files 附件文件列表 * @return this */ public Mail setFiles(File... files) { if (ArrayUtil.isEmpty(files)) { return this; } final DataSource[] attachments = new DataSource[files.length]; for (int i = 0; i < files.length; i++) { attachments[i] = new FileDataSource(files[i]); } return setAttachments(attachments); } /** * 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件 * * @param attachments 附件列表 * @return this * @since 4.0.9 */ public Mail setAttachments(DataSource... attachments) { if (ArrayUtil.isNotEmpty(attachments)) { final Charset charset = this.mailAccount.getCharset(); MimeBodyPart bodyPart; String nameEncoded; try { for (DataSource attachment : attachments) { bodyPart = new MimeBodyPart(); bodyPart.setDataHandler(new DataHandler(attachment)); nameEncoded = attachment.getName(); if (this.mailAccount.isEncodefilename()) { nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset); } // 普通附件文件名 bodyPart.setFileName(nameEncoded); if (StrUtil.startWith(attachment.getContentType(), "image/")) { // 图片附件,用于正文中引用图片 bodyPart.setContentID(nameEncoded); } this.multipart.addBodyPart(bodyPart); } } catch (MessagingException e) { throw new MailException(e); } } return this; } /** * 增加图片,图片的键对应到邮件模板中的占位字符串,图片类型默认为"image/jpeg" * * @param cid 图片与占位符,占位符格式为cid:${cid} * @param imageStream 图片文件 * @return this * @since 4.6.3 */ public Mail addImage(String cid, InputStream imageStream) { return addImage(cid, imageStream, null); } /** * 增加图片,图片的键对应到邮件模板中的占位字符串 * * @param cid 图片与占位符,占位符格式为cid:${cid} * @param imageStream 图片流,不关闭 * @param contentType 图片类型,null赋值默认的"image/jpeg" * @return this * @since 4.6.3 */ public Mail addImage(String cid, InputStream imageStream, String contentType) { ByteArrayDataSource imgSource; try { imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg")); } catch (IOException e) { throw new IORuntimeException(e); } imgSource.setName(cid); return setAttachments(imgSource); } /** * 增加图片,图片的键对应到邮件模板中的占位字符串 * * @param cid 图片与占位符,占位符格式为cid:${cid} * @param imageFile 图片文件 * @return this * @since 4.6.3 */ public Mail addImage(String cid, File imageFile) { InputStream in = null; try { in = FileUtil.getInputStream(imageFile); return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile)); } finally { IoUtil.close(in); } } /** * 设置字符集编码 * * @param charset 字符集编码 * @return this * @see MailAccount#setCharset(Charset) */ public Mail setCharset(Charset charset) { this.mailAccount.setCharset(charset); return this; } /** * 设置是否使用全局会话,默认为true * * @param isUseGlobalSession 是否使用全局会话,默认为true * @return this * @since 4.0.2 */ public Mail setUseGlobalSession(boolean isUseGlobalSession) { this.useGlobalSession = isUseGlobalSession; return this; } /** * 设置debug输出位置,可以自定义debug日志 * * @param debugOutput debug输出位置 * @return this * @since 5.5.6 */ public Mail setDebugOutput(PrintStream debugOutput) { this.debugOutput = debugOutput; return this; } // --------------------------------------------------------------- Getters and Setters end @Override public MimeMessage build() { try { return buildMsg(); } catch (MessagingException e) { throw new MailException(e); } } /** * 发送 * * @return message-id * @throws MailException 邮件发送异常 */ public String send() throws MailException { try { return doSend(); } catch (MessagingException e) { if (e instanceof SendFailedException) { // 当地址无效时,显示更加详细的无效地址信息 final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses(); final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses)); throw new MailException(msg, e); } throw new MailException(e); } } // --------------------------------------------------------------- Private method start /** * 执行发送 * * @return message-id * @throws MessagingException 发送异常 */ private String doSend() throws MessagingException { final MimeMessage mimeMessage = buildMsg(); Transport.send(mimeMessage); return mimeMessage.getMessageID(); } /** * 构建消息 * * @return {@link MimeMessage}消息 * @throws MessagingException 消息异常 */ private MimeMessage buildMsg() throws MessagingException { final Charset charset = this.mailAccount.getCharset(); final MimeMessage msg = new MimeMessage(getSession()); // 发件人 final String from = this.mailAccount.getFrom(); if (StrUtil.isEmpty(from)) { // 用户未提供发送方,则从Session中自动获取 msg.setFrom(); } else { msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset)); } // 标题 msg.setSubject(this.title, (null == charset) ? null : charset.name()); // 发送时间 msg.setSentDate(new Date()); // 内容和附件 msg.setContent(buildContent(charset)); // 收件人 msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset)); // 抄送人 if (ArrayUtil.isNotEmpty(this.ccs)) { msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset)); } // 密送人 if (ArrayUtil.isNotEmpty(this.bccs)) { msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset)); } // 回复地址(reply-to) if (ArrayUtil.isNotEmpty(this.reply)) { msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset)); } return msg; } /** * 构建邮件信息主体 * * @param charset 编码,{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()} * @return 邮件信息主体 * @throws MessagingException 消息异常 */ private Multipart buildContent(Charset charset) throws MessagingException { final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset(); // 正文 final MimeBodyPart body = new MimeBodyPart(); body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr)); this.multipart.addBodyPart(body); return this.multipart; } /** * 获取默认邮件会话
* 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话 * * @return 邮件会话 {@link Session} */
private Session getSession() { final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession); if (null != this.debugOutput) { session.setDebugOut(debugOutput); } return session; } // --------------------------------------------------------------- Private method end }

邮件账户对象

package com.xueyi.common.mail.utils;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.Setting;

import java.io.Serial;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 邮件账户对象
 *
 * @author kevin
 */
public class MailAccount implements Serializable {
    @Serial
    private static final long serialVersionUID = -6937313421815719204L;

    private static final String MAIL_PROTOCOL = "mail.transport.protocol";
    private static final String SMTP_HOST = "mail.smtp.host";
    private static final String SMTP_PORT = "mail.smtp.port";
    private static final String SMTP_AUTH = "mail.smtp.auth";
    private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
    private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
    private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";

    // SSL
    private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
    private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
    private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
    private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
    private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
    private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";

    // System Properties
    private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
    //private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
    //private static final String CHARSET = "mail.mime.charset";

    // 其他
    private static final String MAIL_DEBUG = "mail.debug";

    public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};

    /**
     * SMTP服务器域名
     */
    private String host;
    /**
     * SMTP服务端口
     */
    private Integer port;
    /**
     * 是否需要用户名密码验证
     */
    private Boolean auth;
    /**
     * 用户名
     */
    private String user;
    /**
     * 密码
     */
    private String pass;
    /**
     * 发送方,遵循RFC-822标准
     */
    private String from;

    /**
     * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
     */
    private boolean debug;
    /**
     * 编码用于编码邮件正文和发送人、收件人等中文
     */
    private Charset charset = CharsetUtil.CHARSET_UTF_8;
    /**
     * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
     */
    private boolean splitlongparameters = false;
    /**
     * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
     */
    private boolean encodefilename = true;

    /**
     * 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
     */
    private boolean starttlsEnable = false;
    /**
     * 使用 SSL安全连接
     */
    private Boolean sslEnable;

    /**
     * SSL协议,多个协议用空格分隔
     */
    private String sslProtocols;

    /**
     * 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
     */
    private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
    /**
     * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
     */
    private boolean socketFactoryFallback;
    /**
     * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
     */
    private int socketFactoryPort = 465;

    /**
     * SMTP超时时长,单位毫秒,缺省值不超时
     */
    private long timeout;
    /**
     * Socket连接超时值,单位毫秒,缺省值不超时
     */
    private long connectionTimeout;
    /**
     * Socket写出超时值,单位毫秒,缺省值不超时
     */
    private long writeTimeout;

    /**
     * 自定义的其他属性,此自定义属性会覆盖默认属性
     */
    private final Map<String, Object> customProperty = new HashMap<>();

    // -------------------------------------------------------------- Constructor start

    /**
     * 构造,所有参数需自行定义或保持默认值
     */
    public MailAccount() {
    }

    /**
     * 构造
     *
     * @param settingPath 配置文件路径
     */
    public MailAccount(String settingPath) {
        this(new Setting(settingPath));
    }

    /**
     * 构造
     *
     * @param setting 配置文件
     */
    public MailAccount(Setting setting) {
        setting.toBean(this);
    }

    // -------------------------------------------------------------- Constructor end

    /**
     * 获得SMTP服务器域名
     *
     * @return SMTP服务器域名
     */
    public String getHost() {
        return host;
    }

    /**
     * 设置SMTP服务器域名
     *
     * @param host SMTP服务器域名
     * @return this
     */
    public MailAccount setHost(String host) {
        this.host = host;
        return this;
    }

    /**
     * 获得SMTP服务端口
     *
     * @return SMTP服务端口
     */
    public Integer getPort() {
        return port;
    }

    /**
     * 设置SMTP服务端口
     *
     * @param port SMTP服务端口
     * @return this
     */
    public MailAccount setPort(Integer port) {
        this.port = port;
        return this;
    }

    /**
     * 是否需要用户名密码验证
     *
     * @return 是否需要用户名密码验证
     */
    public Boolean isAuth() {
        return auth;
    }

    /**
     * 设置是否需要用户名密码验证
     *
     * @param isAuth 是否需要用户名密码验证
     * @return this
     */
    public MailAccount setAuth(boolean isAuth) {
        this.auth = isAuth;
        return this;
    }

    /**
     * 获取用户名
     *
     * @return 用户名
     */
    public String getUser() {
        return user;
    }

    /**
     * 设置用户名
     *
     * @param user 用户名
     * @return this
     */
    public MailAccount setUser(String user) {
        this.user = user;
        return this;
    }

    /**
     * 获取密码
     *
     * @return 密码
     */
    public String getPass() {
        return pass;
    }

    /**
     * 设置密码
     *
     * @param pass 密码
     * @return this
     */
    public MailAccount setPass(String pass) {
        this.pass = pass;
        return this;
    }

    /**
     * 获取发送方,遵循RFC-822标准
     *
     * @return 发送方,遵循RFC-822标准
     */
    public String getFrom() {
        return from;
    }

    /**
     * 设置发送方,遵循RFC-822标准
* 发件人可以是以下形式: * *
     * 1. [email protected]
     * 2.  name <[email protected]>
     * 
* * @param from 发送方,遵循RFC-822标准 * @return this */
public MailAccount setFrom(String from) { this.from = from; return this; } /** * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 * * @return 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 * @since 4.0.2 */ public boolean isDebug() { return debug; } /** * 设置是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 * * @param debug 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 * @return this * @since 4.0.2 */ public MailAccount setDebug(boolean debug) { this.debug = debug; return this; } /** * 获取字符集编码 * * @return 编码,可能为{@code null} */ public Charset getCharset() { return charset; } /** * 设置字符集编码,此选项不会修改全局配置,若修改全局配置,请设置此项为{@code null}并设置: *
     * 	System.setProperty("mail.mime.charset", charset);
     * 
* * @param charset 字符集编码,{@code null} 则表示使用全局设置的默认编码,全局编码为mail.mime.charset系统属性 * @return this */
public MailAccount setCharset(Charset charset) { this.charset = charset; return this; } /** * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名) * * @return 对于超长参数是否切分为多份 */ public boolean isSplitlongparameters() { return splitlongparameters; } /** * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
* 注意此项为全局设置,此项会调用 *
     * System.setProperty("mail.mime.splitlongparameters", true)
     * 
* * @param splitlongparameters 对于超长参数是否切分为多份 */
public void setSplitlongparameters(boolean splitlongparameters) { this.splitlongparameters = splitlongparameters; } /** * 对于文件名是否使用{@link #charset}编码,默认为 {@code true} * * @return 对于文件名是否使用{@link #charset}编码,默认为 {@code true} * @since 5.7.16 */ public boolean isEncodefilename() { return encodefilename; } /** * 设置对于文件名是否使用{@link #charset}编码,此选项不会修改全局配置
* 如果此选项设置为{@code false},则是否编码取决于两个系统属性: *
    *
  • mail.mime.encodefilename 是否编码附件文件名
  • *
  • mail.mime.charset 编码文件名的编码
  • *
* * @param encodefilename 对于文件名是否使用{@link #charset}编码 * @since 5.7.16 */
public void setEncodefilename(boolean encodefilename) { this.encodefilename = encodefilename; } /** * 是否使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 * * @return 是否使用 STARTTLS安全连接 */ public boolean isStarttlsEnable() { return this.starttlsEnable; } /** * 设置是否使用STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 * * @param startttlsEnable 是否使用STARTTLS安全连接 * @return this */ public MailAccount setStarttlsEnable(boolean startttlsEnable) { this.starttlsEnable = startttlsEnable; return this; } /** * 是否使用 SSL安全连接 * * @return 是否使用 SSL安全连接 */ public Boolean isSslEnable() { return this.sslEnable; } /** * 设置是否使用SSL安全连接 * * @param sslEnable 是否使用SSL安全连接 * @return this */ public MailAccount setSslEnable(Boolean sslEnable) { this.sslEnable = sslEnable; return this; } /** * 获取SSL协议,多个协议用空格分隔 * * @return SSL协议,多个协议用空格分隔 * @since 5.5.7 */ public String getSslProtocols() { return sslProtocols; } /** * 设置SSL协议,多个协议用空格分隔 * * @param sslProtocols SSL协议,多个协议用空格分隔 * @since 5.5.7 */ public void setSslProtocols(String sslProtocols) { this.sslProtocols = sslProtocols; } /** * 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 * * @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字 */ public String getSocketFactoryClass() { return socketFactoryClass; } /** * 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 * * @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 * @return this */ public MailAccount setSocketFactoryClass(String socketFactoryClass) { this.socketFactoryClass = socketFactoryClass; return this; } /** * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true * * @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true */ public boolean isSocketFactoryFallback() { return socketFactoryFallback; } /** * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true * * @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true * @return this */ public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) { this.socketFactoryFallback = socketFactoryFallback; return this; } /** * 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 * * @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 */ public int getSocketFactoryPort() { return socketFactoryPort; } /** * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 * * @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 * @return this */ public MailAccount setSocketFactoryPort(int socketFactoryPort) { this.socketFactoryPort = socketFactoryPort; return this; } /** * 设置SMTP超时时长,单位毫秒,缺省值不超时 * * @param timeout SMTP超时时长,单位毫秒,缺省值不超时 * @return this * @since 4.1.17 */ public MailAccount setTimeout(long timeout) { this.timeout = timeout; return this; } /** * 设置Socket连接超时值,单位毫秒,缺省值不超时 * * @param connectionTimeout Socket连接超时值,单位毫秒,缺省值不超时 * @return this * @since 4.1.17 */ public MailAccount setConnectionTimeout(long connectionTimeout) { this.connectionTimeout = connectionTimeout; return this; } /** * 设置Socket写出超时值,单位毫秒,缺省值不超时 * * @param writeTimeout Socket写出超时值,单位毫秒,缺省值不超时 * @return this * @since 5.8.3 */ public MailAccount setWriteTimeout(long writeTimeout) { this.writeTimeout = writeTimeout; return this; } /** * 获取自定义属性列表 * * @return 自定义参数列表 * @since 5.6.4 */ public Map<String, Object> getCustomProperty() { return customProperty; } /** * 设置自定义属性,如mail.smtp.ssl.socketFactory * * @param key 属性名,空白被忽略 * @param value 属性值, null被忽略 * @return this * @since 5.6.4 */ public MailAccount setCustomProperty(String key, Object value) { if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) { this.customProperty.put(key, value); } return this; } /** * 获得SMTP相关信息 * * @return {@link Properties} */ public Properties getSmtpProps() { //全局系统参数 System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters)); final Properties p = new Properties(); p.put(MAIL_PROTOCOL, "smtp"); p.put(SMTP_HOST, this.host); p.put(SMTP_PORT, String.valueOf(this.port)); p.put(SMTP_AUTH, String.valueOf(this.auth)); if (this.timeout > 0) { p.put(SMTP_TIMEOUT, String.valueOf(this.timeout)); } if (this.connectionTimeout > 0) { p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout)); } // issue#2355 if (this.writeTimeout > 0) { p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout)); } p.put(MAIL_DEBUG, String.valueOf(this.debug)); if (this.starttlsEnable) { //STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 p.put(STARTTLS_ENABLE, "true"); if (null == this.sslEnable) { //为了兼容旧版本,当用户没有此项配置时,按照starttlsEnable开启状态时对待 this.sslEnable = true; } } // SSL if (null != this.sslEnable && this.sslEnable) { p.put(SSL_ENABLE, "true"); p.put(SOCKET_FACTORY, socketFactoryClass); p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback)); p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort)); // issue#IZN95@Gitee,在Linux下需自定义SSL协议版本 if (StrUtil.isNotBlank(this.sslProtocols)) { p.put(SSL_PROTOCOLS, this.sslProtocols); } } // 补充自定义属性,允许自定属性覆盖已经设置的值 p.putAll(this.customProperty); return p; } /** * 如果某些值为null,使用默认值 * * @return this */ public MailAccount defaultIfEmpty() { // 去掉发件人的姓名部分 final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress(); if (StrUtil.isBlank(this.host)) { // 如果SMTP地址为空,默认使用smtp.<发件人邮箱后缀> this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1)); } if (StrUtil.isBlank(user)) { // 如果用户名为空,默认为发件人(issue#I4FYVY@Gitee) //this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@')); this.user = fromAddress; } if (null == this.auth) { // 如果密码非空白,则使用认证模式 this.auth = (false == StrUtil.isBlank(this.pass)); } if (null == this.port) { // 端口在SSL状态下默认与socketFactoryPort一致,非SSL状态下默认为25 this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25; } if (null == this.charset) { // 默认UTF-8编码 this.charset = CharsetUtil.CHARSET_UTF_8; } return this; } @Override public String toString() { return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable=" + starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]"; } }

邮件异常工具类

package com.xueyi.common.mail.utils;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;

import java.io.Serial;

/**
 * 邮件异常
 *
 * @author kevin
 */
public class MailException extends RuntimeException {
    @Serial
    private static final long serialVersionUID = 8247610319171014183L;

    public MailException(Throwable e) {
        super(ExceptionUtil.getMessage(e), e);
    }

    public MailException(String message) {
        super(message);
    }

    public MailException(String messageTemplate, Object... params) {
        super(StrUtil.format(messageTemplate, params));
    }

    public MailException(String message, Throwable throwable) {
        super(message, throwable);
    }

    public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
        super(message, throwable, enableSuppression, writableStackTrace);
    }

    public MailException(Throwable throwable, String messageTemplate, Object... params) {
        super(StrUtil.format(messageTemplate, params), throwable);
    }
}

邮件工具类

package com.xueyi.common.mail.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.mail.Authenticator;
import jakarta.mail.Session;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.io.File;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;


/**
 * 邮件工具类
 *
 * @author kevin
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MailUtils {

    private static final MailAccount ACCOUNT = SpringUtil.getBean(MailAccount.class);

    /**
     * 获取邮件发送实例
     */
    public static MailAccount getMailAccount() {
        return ACCOUNT;
    }

    /**
     * 获取邮件发送实例 (自定义发送人以及授权码)
     *
     * @param user 发送人
     * @param pass 授权码
     */
    public static MailAccount getMailAccount(String from, String user, String pass) {
        ACCOUNT.setFrom(StrUtil.blankToDefault(from, ACCOUNT.getFrom()));
        ACCOUNT.setUser(StrUtil.blankToDefault(user, ACCOUNT.getUser()));
        ACCOUNT.setPass(StrUtil.blankToDefault(pass, ACCOUNT.getPass()));
        return ACCOUNT;
    }

    /**
     * 使用配置文件中设置的账户发送文本邮件,发送给单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人 * @param subject 标题 * @param content 正文 * @param files 附件列表 * @return message-id * @since 3.2.0 */
public static String sendText(String to, String subject, String content, File... files) { return send(to, subject, content, false, files); } /** * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人 * @param subject 标题 * @param content 正文 * @param files 附件列表 * @return message-id * @since 3.2.0 */
public static String sendHtml(String to, String subject, String content, File... files) { return send(to, subject, content, true, files); } /** * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id */
public static String send(String to, String subject, String content, boolean isHtml, File... files) { return send(splitAddress(to), subject, content, isHtml, files); } /** * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
* 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id * @since 4.0.3 */
public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) { return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files); } /** * 使用配置文件中设置的账户发送文本邮件,发送给多人 * * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param files 附件列表 * @return message-id */ public static String sendText(Collection<String> tos, String subject, String content, File... files) { return send(tos, subject, content, false, files); } /** * 使用配置文件中设置的账户发送HTML邮件,发送给多人 * * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param files 附件列表 * @return message-id * @since 3.2.0 */ public static String sendHtml(Collection<String> tos, String subject, String content, File... files) { return send(tos, subject, content, true, files); } /** * 使用配置文件中设置的账户发送邮件,发送给多人 * * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id */ public static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) { return send(tos, null, null, subject, content, isHtml, files); } /** * 使用配置文件中设置的账户发送邮件,发送给多人 * * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id * @since 4.0.3 */ public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) { return send(getMailAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files); } // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount /** * 发送邮件给多人 * * @param mailAccount 邮件认证对象 * @param to 收件人,多个收件人逗号或者分号隔开 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 3.2.0 */ public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) { return send(mailAccount, splitAddress(to), subject, content, isHtml, files); } /** * 发送邮件给多人 * * @param mailAccount 邮件帐户信息 * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id */ public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) { return send(mailAccount, tos, null, null, subject, content, isHtml, files); } /** * 发送邮件给多人 * * @param mailAccount 邮件帐户信息 * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 4.0.3 */ public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) { return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files); } /** * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param files 附件列表 * @return message-id * @since 3.2.0 */
public static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) { return send(to, subject, content, imageMap, true, files); } /** * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id */
public static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(splitAddress(to), subject, content, imageMap, isHtml, files); } /** * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
* 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔 * * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id * @since 4.0.3 */
public static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files); } /** * 使用配置文件中设置的账户发送HTML邮件,发送给多人 * * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param files 附件列表 * @return message-id * @since 3.2.0 */ public static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) { return send(tos, subject, content, imageMap, true, files); } /** * 使用配置文件中设置的账户发送邮件,发送给多人 * * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id */ public static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(tos, null, null, subject, content, imageMap, isHtml, files); } /** * 使用配置文件中设置的账户发送邮件,发送给多人 * * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML * @param files 附件列表 * @return message-id * @since 4.0.3 */ public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(getMailAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files); } // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount /** * 发送邮件给多人 * * @param mailAccount 邮件认证对象 * @param to 收件人,多个收件人逗号或者分号隔开 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 3.2.0 */ public static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files); } /** * 发送邮件给多人 * * @param mailAccount 邮件帐户信息 * @param tos 收件人列表 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 4.6.3 */ public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files); } /** * 发送邮件给多人 * * @param mailAccount 邮件帐户信息 * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 4.6.3 */ public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files); } /** * 根据配置文件,获取邮件客户端会话 * * @param mailAccount 邮件账户配置 * @param isSingleton 是否单例(全局共享会话) * @return {@link Session} * @since 5.5.7 */ public static Session getSession(MailAccount mailAccount, boolean isSingleton) { Authenticator authenticator = null; if (mailAccount.isAuth()) { authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass()); } return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) // : Session.getInstance(mailAccount.getSmtpProps(), authenticator); } // ------------------------------------------------------------------------------------------------------------------------ Private method start /** * 发送邮件给多人 * * @param mailAccount 邮件帐户信息 * @param useGlobalSession 是否全局共享Session * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 * @param imageMap 图片与占位符,占位符格式为cid:${cid} * @param isHtml 是否为HTML格式 * @param files 附件列表 * @return message-id * @since 4.6.3 */ private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) { final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession); // 可选抄送人 if (CollUtil.isNotEmpty(ccs)) { mail.setCcs(ccs.toArray(new String[0])); } // 可选密送人 if (CollUtil.isNotEmpty(bccs)) { mail.setBccs(bccs.toArray(new String[0])); } mail.setTos(tos.toArray(new String[0])); mail.setTitle(subject); mail.setContent(content); mail.setHtml(isHtml); mail.setFiles(files); // 图片 if (MapUtil.isNotEmpty(imageMap)) { for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) { mail.addImage(entry.getKey(), entry.getValue()); // 关闭流 IoUtil.close(entry.getValue()); } } return mail.send(); } /** * 将多个联系人转为列表,分隔符为逗号或者分号 * * @param addresses 多个联系人,如果为空返回null * @return 联系人列表 */ private static List<String> splitAddress(String addresses) { if (StrUtil.isBlank(addresses)) { return null; } List<String> result; if (StrUtil.contains(addresses, CharUtil.COMMA)) { result = StrUtil.splitTrim(addresses, CharUtil.COMMA); } else if (StrUtil.contains(addresses, ';')) { result = StrUtil.splitTrim(addresses, ';'); } else { result = CollUtil.newArrayList(addresses); } return result; } // ------------------------------------------------------------------------------------------------------------------------ Private method end }

邮件用户名密码验证器

package com.xueyi.common.mail.utils;

import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;

/**
 * 用户名密码验证器
 *
 * @author kevin
 */
public class UserPassAuthenticator extends Authenticator {

    private final String user;
    private final String pass;

    /**
     * 构造
     *
     * @param user 用户名
     * @param pass 密码
     */
    public UserPassAuthenticator(String user, String pass) {
        this.user = user;
        this.pass = pass;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(this.user, this.pass);
    }

}

Redis使用FastJson序列化

package com.xueyi.common.redis.configure;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import com.xueyi.common.core.constant.basic.Constants;
import com.xueyi.common.core.utils.core.ArrayUtil;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Redis使用FastJson序列化
 *
 * @author xueyi
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (ArrayUtil.isEmpty(bytes)) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
    }
}

redis配置

package com.xueyi.common.redis.configure;

import com.xueyi.common.core.utils.core.ObjectUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import lombok.Data;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 *
 * @author xueyi
 */
@Data
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisConfig {

    /** 地址 */
    private String host;

    /** 端口 */
    private Integer port;

    /** 密码 */
    private String password;

    /** 库 */
    private int database;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration(host, port);
        if (StrUtil.isNotBlank(password)) {
            conf.setPassword(password);
        }
        if (ObjectUtil.isNotNull(database)) {
            conf.setDatabase(database);
        }
        return new LettuceConnectionFactory(conf);
    }

    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

    @Bean("redisJavaTemplate")
    public RedisTemplate<Object, Object> redisJavaTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(RedisSerializer.java());

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(RedisSerializer.java());

        template.afterPropertiesSet();
        return template;
    }
}

缓存 通用常量

package com.xueyi.common.redis.constant;

import com.xueyi.common.core.utils.core.EnumUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 缓存 通用常量
 *
 * @author xueyi
 */
public class RedisConstants {

    /** 缓存键 */
    @Getter
    @AllArgsConstructor
    public enum CacheKey {

        CLIENT_DETAILS_KEY("client:details", "oauth 客户端信息"),
        CAPTCHA_CODE_KEY("captcha_codes:", "验证码"),
        SYS_CORRELATE_KEY("system:correlate:{}.{}", "数据关联工具"),
        SYS_CORRELATE_IMPL_KEY("system:correlate:service:{}", "数据关联工具"),
        DICT_KEY("system:dict:{}", "字典缓存"),
        CONFIG_KEY("system:config:{}", "参数缓存");

        private final String code;
        private final String info;

        public String getCacheName(Object... cacheNames) {
            return StrUtil.format(getCode(), cacheNames);
        }
    }

    /** 缓存类型 */
    @Getter
    @AllArgsConstructor
    public enum OperateType {

        REFRESH_ALL("refresh_all", "更新全部"),
        REFRESH("refresh", "更新"),
        REMOVE("remove", "删除");

        private final String code;
        private final String info;

        public static OperateType getByCode(String code) {
            return EnumUtil.getByCode(OperateType.class, code);
        }

    }

}

spring redis 工具类

package com.xueyi.common.redis.service;

import com.xueyi.common.core.exception.UtilException;
import com.xueyi.common.core.utils.core.ArrayUtil;
import com.xueyi.common.core.utils.core.CollUtil;
import com.xueyi.common.core.utils.core.MapUtil;
import com.xueyi.common.core.utils.core.NumberUtil;
import com.xueyi.common.core.utils.core.ObjectUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * spring redis 工具类
 *
 * @author xueyi
 **/
@Component
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class RedisService {

    @Autowired
    public RedisTemplate redisTemplate;

    @Autowired
    @Qualifier("redisJavaTemplate")
    public RedisTemplate redisJavaTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等 | Java序列化
     *
     * @param key   Redis键
     * @param value 缓存的值
     */
    public <T> void setJavaCacheObject(final String key, final T value) {
        setJavaCacheObject(key, value, NumberUtil.Nine, TimeUnit.HOURS);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等 | Java序列化
     *
     * @param key      Redis键
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setJavaCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit) {
        redisJavaTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 获得缓存的基本对象 | Java序列化
     *
     * @param key Redis键
     * @return 缓存键值对应的数据
     */
    public <T> T getJavaCacheObject(final String key) {
        ValueOperations<String, T> operation = redisJavaTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 缓存Map | Java序列化
     *
     * @param key     Redis键
     * @param dataMap map
     */
    public <T> void setJavaCacheMap(final String key, final Map<String, T> dataMap) {
        setJavaCacheMap(key, dataMap, NumberUtil.Nine, TimeUnit.HOURS);
    }

    /**
     * 缓存Map | Java序列化
     *
     * @param key     Redis键
     * @param dataMap map
     * @param timeout 超时时间
     * @param unit    时间单位
     */
    public <T> void setJavaCacheMap(final String key, final Map<String, T> dataMap, final long timeout, final TimeUnit unit) {
        if (dataMap != null) {
            redisJavaTemplate.opsForHash().putAll(key, dataMap);
            expireJava(key, timeout, unit);
        }
    }

    /**
     * 获得缓存的Map | Java序列化
     *
     * @param key Redis键
     * @return 哈希
     */
    public <T> Map<String, T> getJavaCacheMap(final String key) {
        return redisJavaTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据 | Java序列化
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setJavaCacheMapValue(final String key, final String hKey, final T value) {
        redisJavaTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据 | Java序列化
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getJavaCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisJavaTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 判断 key是否存在 | Java序列化
     *
     * @param key Redis键
     * @return true 存在 false不存在
     */
    public Boolean hasJavaKey(String key) {
        return redisJavaTemplate.hasKey(key);
    }

    /**
     * 设置有效时间 | Java序列化
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public Boolean expireJava(final String key, final long timeout, final TimeUnit unit) {
        return redisJavaTemplate.expire(key, timeout, unit);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   Redis键
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      Redis键
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public Boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public Long getExpire(final String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key Redis键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 获得缓存的基本对象
     *
     * @param key Redis键
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key Redis键
     * @return true=删除成功;false=删除失败
     */
    public Boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return 结果
     */
    public Long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      Redis键
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 缓存List数据
     *
     * @param key      Redis键
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList, final long timeout, final TimeUnit unit) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        if (ObjectUtil.isNotNull(count)) {
            expire(key, timeout, unit);
        }
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key Redis键
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     Redis键
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        for (T t : dataSet) {
            setOperation.add(t);
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key Redis键
     * @return 集合
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key     Redis键
     * @param dataMap map
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 缓存Map
     *
     * @param key     Redis键
     * @param dataMap map
     * @param timeout 超时时间
     * @param unit    时间单位
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap, final long timeout, final TimeUnit unit) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
            expire(key, timeout, unit);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key Redis键
     * @return 哈希
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey) {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final Object[] hKey) {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 更新LIST缓存字典
     *
     * @param optionKey 集合缓存键值
     * @param dataList  数据集合
     */
    public <T> void refreshListCache(String optionKey, List<T> dataList) {
        setCacheList(optionKey, dataList, NumberUtil.Nine, TimeUnit.HOURS);
    }

    /**
     * 全量更新MAP缓存字典
     *
     * @param mapKey    缓存键值
     * @param cacheList 缓存数据集合
     * @param keyGet    键名
     * @param valueGet  值名
     */
    public <T, K> void refreshMapCache(String mapKey, Collection<T> cacheList, Function<? super T, String> keyGet, Function<? super T, K> valueGet) {
        Map<String, K> resultMap = new HashMap<>();
        if (CollUtil.isNotEmpty(cacheList)) {
            resultMap = cacheList.stream()
                    .filter(item -> {
                        if (StrUtil.isEmpty(keyGet.apply(item))) {
                            throw new UtilException("cache key cannot be empty");
                        }
                        return ObjectUtil.isNotNull(valueGet.apply(item));
                    }).collect(Collectors.toMap(keyGet, valueGet));
        }
        if (MapUtil.isNotEmpty(resultMap)) {
            setCacheMap(mapKey, resultMap, NumberUtil.Nine, TimeUnit.HOURS);
        } else {
            deleteObject(mapKey);
        }
    }

    /**
     * 更新MAP缓存字典
     *
     * @param mapKey   缓存键值
     * @param keyGet   键名
     * @param valueGet 值名
     */
    public <T> void refreshMapValueCache(String mapKey, Supplier<Serializable> keyGet, Supplier<T> valueGet) {
        if (ObjectUtil.isNotNull(valueGet.get())) {
            setCacheMapValue(mapKey, keyGet.get().toString(), valueGet.get());
        } else {
            deleteCacheMapValue(mapKey, keyGet.get().toString());
        }
        if (hasKey(mapKey)) {
            expire(mapKey, NumberUtil.Nine, TimeUnit.HOURS);
        }
    }

    /**
     * 删除MAP缓存字典指定键数据
     *
     * @param mapKey 缓存键值
     * @param keys   键名
     */
    public void removeMapValueCache(String mapKey, Serializable... keys) {
        if (ArrayUtil.isNotEmpty(keys)) {
            deleteCacheMapValue(mapKey, keys);
        }
    }
}

Token解析器

package com.xueyi.common.security.handler;

import com.xueyi.common.core.constant.basic.SecurityConstants;
import com.xueyi.common.core.constant.basic.TokenConstants;
import com.xueyi.common.core.utils.core.ArrayUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import com.xueyi.common.core.utils.servlet.ServletUtil;
import com.xueyi.common.security.config.properties.PermitAllUrlProperties;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Token解析器
 *
 * @author xueyi
 */
public class BearerTokenHandler implements BearerTokenResolver {

    private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-:._~+/]+=*)$", Pattern.CASE_INSENSITIVE);

    private boolean allowFormEncodedBodyParameter = false;

    private boolean allowUriQueryParameter = true;

    private final PathMatcher pathMatcher = new AntPathMatcher();

    private final PermitAllUrlProperties urlProperties;

    public BearerTokenHandler(PermitAllUrlProperties urlProperties) {
        this.urlProperties = urlProperties;
    }

    @Override
    public String resolve(HttpServletRequest request) {
        AtomicBoolean match = new AtomicBoolean(urlProperties.getRoutine().stream().anyMatch(url ->
                pathMatcher.match(url, request.getRequestURI())));

        if (!match.get()) {
            Optional.ofNullable(urlProperties.getCustom().get(RequestMethod.valueOf(request.getMethod())))
                    .ifPresent(list -> match.set(list.stream().anyMatch(url -> pathMatcher.match(url, request.getRequestURI()))));
        }
        if (match.get()) {
            return null;
        }

        final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
        final String parameterToken = isParameterTokenSupportedForRequest(request) ? resolveFromRequestParameters(request) : null;
        if (authorizationHeaderToken != null) {
            if (parameterToken != null) {
                final BearerTokenError error = BearerTokenErrors
                        .invalidRequest("Found multiple bearer tokens in the request");
                throw new OAuth2AuthenticationException(error);
            }
            return authorizationHeaderToken;
        }
        if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
            return parameterToken;
        }
        return null;
    }

    private String resolveFromAuthorizationHeader(HttpServletRequest request) {
        String authorization = ServletUtil.getHeader(request, SecurityConstants.BaseSecurity.ACCESS_TOKEN.getCode());
        if (!StrUtil.startWith(authorization, TokenConstants.PREFIX))
            return null;
        Matcher matcher = authorizationPattern.matcher(authorization);
        if (!matcher.matches()) {
            BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
            throw new OAuth2AuthenticationException(error);
        }
        return matcher.group("token");
    }

    private static String resolveFromRequestParameters(HttpServletRequest request) {
        String[] values = request.getParameterValues(SecurityConstants.BaseSecurity.ACCESS_TOKEN.getCode());
        if (ArrayUtil.isEmpty(values)) {
            return null;
        }
        if (values.length == 1) {
            return values[0];
        }
        BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
        throw new OAuth2AuthenticationException(error);
    }

    private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
        return (("POST".equals(request.getMethod())
                && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
                || "GET".equals(request.getMethod()));
    }

    private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
        return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
                && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
                || (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
    }
}

Redis授权认证器

package com.xueyi.common.security.handler;

import com.xueyi.common.core.utils.core.NumberUtil;
import com.xueyi.common.redis.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.concurrent.TimeUnit;

/**
 * Redis授权认证器
 *
 * @author xueyi
 */
@Component
public class RedisOAuth2AuthorizationConsentHandler implements OAuth2AuthorizationConsentService {

    @Autowired
    private RedisService redisService;

    @Override
    public void save(OAuth2AuthorizationConsent authorizationConsent) {
        Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
        redisService.setCacheObject(buildKey(authorizationConsent), authorizationConsent, (long) NumberUtil.Ten, TimeUnit.MINUTES);
    }

    @Override
    public void remove(OAuth2AuthorizationConsent authorizationConsent) {
        Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
        redisService.deleteObject(buildKey(authorizationConsent));
    }

    @Override
    public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
        Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
        Assert.hasText(principalName, "principalName cannot be empty");
        OAuth2AuthorizationConsent authorizationConsent = redisService.getCacheObject(buildKey(registeredClientId, principalName));
        return authorizationConsent;
    }

    private static String buildKey(String registeredClientId, String principalName) {
        return "token:consent:" + registeredClientId + ":" + principalName;
    }

    private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
        return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
    }

}

Redis授权

package com.xueyi.common.security.handler;

import com.xueyi.common.core.utils.core.ObjectUtil;
import com.xueyi.common.core.web.model.BaseLoginUser;
import com.xueyi.common.redis.service.RedisService;
import com.xueyi.common.security.service.ITokenService;
import com.xueyi.common.security.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.Objects;
import java.util.Optional;

/**
 * Redis授权
 *
 * @author xueyi
 */
@Component
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class RedisOAuth2AuthorizationHandler implements OAuth2AuthorizationService {

    @Autowired
    private RedisService redisService;

    @Override
    public void save(OAuth2Authorization authorization) {
        // check loginUser info
        BaseLoginUser loginUser = SecurityUtils.getLoginUser(authorization);
        // check token service
        ITokenService tokenService = SecurityUtils.getTokenService(loginUser.getAccountType().getCode());

        // build refresh token
        if (isRefreshToken(authorization)) {
            String refreshToken = Optional.ofNullable(authorization.getRefreshToken())
                    .map(OAuth2Authorization.Token::getToken)
                    .map(OAuth2RefreshToken::getTokenValue)
                    .orElseThrow(() -> new NullPointerException("refreshToken cannot be null"));
            loginUser.setRefreshToken(tokenService.getTokenAddress(OAuth2ParameterNames.REFRESH_TOKEN, loginUser.getEnterpriseId(), loginUser.getUserId(), refreshToken));
        }

        // build access token
        if (isAccessToken(authorization)) {
            String accessToken = Optional.ofNullable(authorization.getAccessToken())
                    .map(OAuth2Authorization.Token::getToken)
                    .map(OAuth2AccessToken::getTokenValue)
                    .orElseThrow(() -> new NullPointerException("accessToken cannot be null"));
            loginUser.setAccessToken(tokenService.getTokenAddress(OAuth2ParameterNames.ACCESS_TOKEN, loginUser.getEnterpriseId(), loginUser.getUserId(), accessToken));
        }

        // build state token
        if (isState(authorization)) {
            String token = authorization.getAttribute(OAuth2ParameterNames.STATE);
            loginUser.setStateToken(tokenService.getTokenAddress(OAuth2ParameterNames.STATE, loginUser.getEnterpriseId(), loginUser.getUserId(), token));
        }

        // build code token
        if (isCode(authorization)) {
            OAuth2AuthorizationCode authorizationCodeToken = Optional.ofNullable(authorization.getToken(OAuth2AuthorizationCode.class))
                    .map(OAuth2Authorization.Token::getToken)
                    .orElseThrow(() -> new NullPointerException("authorizationCodeToken cannot be null"));
            loginUser.setCodeToken(tokenService.getTokenAddress(OAuth2ParameterNames.CODE, loginUser.getEnterpriseId(), loginUser.getUserId(), authorizationCodeToken.getTokenValue()));
        }

        // build redis cache
        tokenService.createTokenCache(authorization);
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        // check loginUser info
        BaseLoginUser loginUser = SecurityUtils.getLoginUser(authorization);
        // check token service
        ITokenService tokenService = SecurityUtils.getTokenService(loginUser.getAccountType().getCode());
        tokenService.removeTokenCache(loginUser);
    }

    @Override
    @Nullable
    public OAuth2Authorization findById(String id) {
        throw new UnsupportedOperationException();
    }

    @Override
    @Nullable
    public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
        Assert.hasText(token, "token cannot be empty");
        Assert.notNull(tokenType, "tokenType cannot be empty");
        return ObjectUtil.equals(OAuth2TokenType.ACCESS_TOKEN, tokenType) ? redisService.getJavaCacheObject(token) : ObjectUtil.equals(OAuth2TokenType.REFRESH_TOKEN, tokenType) ?  redisService.getCacheObject(token) : null;
    }

    private static boolean isState(OAuth2Authorization authorization) {
        return Objects.nonNull(authorization.getAttribute(OAuth2ParameterNames.STATE));
    }

    private static boolean isCode(OAuth2Authorization authorization) {
        OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class);
        return ObjectUtil.isNotNull(authorizationCode);
    }

    private static boolean isRefreshToken(OAuth2Authorization authorization) {
        return ObjectUtil.isNotNull(authorization.getRefreshToken());
    }

    private static boolean isAccessToken(OAuth2Authorization authorization) {
        return ObjectUtil.isNotNull(authorization.getAccessToken());
    }
}

文件上传工具类

package com.xueyi.file.utils;

import com.xueyi.common.core.exception.file.FileException;
import com.xueyi.common.core.exception.file.FileNameLengthLimitExceededException;
import com.xueyi.common.core.exception.file.FileSizeLimitExceededException;
import com.xueyi.common.core.exception.file.InvalidExtensionException;
import com.xueyi.common.core.utils.DateUtil;
import com.xueyi.common.core.utils.core.IdUtil;
import com.xueyi.common.core.utils.core.StrUtil;
import com.xueyi.common.core.utils.file.FileTypeUtil;
import com.xueyi.common.core.utils.file.MimeTypeUtil;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Objects;

/**
 * 文件上传工具类
 *
 * @author xueyi
 */
public class FileUploadUtils {

    /** 默认大小 50M */
    public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024;

    /** 默认的文件名最大长度 100 */
    public static final int DEFAULT_FILE_NAME_LENGTH = 100;

    /**
     * 根据文件路径上传
     *
     * @param baseDir 相对应用的基目录
     * @param file    上传的文件
     * @return 文件名称
     */
    public static String upload(String baseDir, MultipartFile file) throws IOException {
        try {
            return upload(baseDir, file, MimeTypeUtil.DEFAULT_ALLOWED_EXTENSION);
        } catch (FileException fe) {
            throw new IOException(fe.getDefaultMessage(), fe);
        } catch (Exception e) {
            throw new IOException(e.getMessage(), e);
        }
    }

    /**
     * 文件上传
     *
     * @param baseDir          相对应用的基目录
     * @param file             上传的文件
     * @param allowedExtension 上传文件类型
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException       如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException                          比如读写文件出错时
     * @throws InvalidExtensionException            文件校验异常
     */
    public static String upload(String baseDir, MultipartFile file, String[] allowedExtension) throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, InvalidExtensionException {
        int fileNameLength = Objects.requireNonNull(file.getOriginalFilename()).length();
        if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }

        assertAllowed(file, allowedExtension);

        String fileName = extractFilename(file);

        String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
        file.transferTo(Paths.get(absPath));
        return getPathFileName(fileName);
    }

    /**
     * 编码文件名
     */
    public static String extractFilename(MultipartFile file) {
        return StrUtil.format("{}/{}_{}.{}", DateUtil.datePath(), FilenameUtils.getBaseName(file.getOriginalFilename()), IdUtil.simpleUUID(), FileTypeUtil.getExtension(file));
    }

    private static File getAbsoluteFile(String uploadDir, String fileName) throws IOException {
        File desc = new File(uploadDir + File.separator + fileName);

        if (!desc.exists()) {
            if (!desc.getParentFile().exists()) {
                desc.getParentFile().mkdirs();
            }
        }
        return desc.isAbsolute() ? desc : desc.getAbsoluteFile();
    }

    private static String getPathFileName(String fileName) {
        return StrUtil.SLASH + fileName;
    }

    /**
     * 文件大小校验
     *
     * @param file 上传的文件
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws InvalidExtensionException      文件校验异常
     */
    public static void assertAllowed(MultipartFile file, String[] allowedExtension) throws FileSizeLimitExceededException, InvalidExtensionException {
        long size = file.getSize();
        if (size > DEFAULT_MAX_SIZE) {
            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
        }

        String fileName = file.getOriginalFilename();
        String extension = FileTypeUtil.getExtension(file);
        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) {
            if (allowedExtension == MimeTypeUtil.IMAGE_EXTENSION) {
                throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, fileName);
            } else if (allowedExtension == MimeTypeUtil.FLASH_EXTENSION) {
                throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, fileName);
            } else if (allowedExtension == MimeTypeUtil.MEDIA_EXTENSION) {
                throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, fileName);
            } else if (allowedExtension == MimeTypeUtil.VIDEO_EXTENSION) {
                throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, fileName);
            } else {
                throw new InvalidExtensionException(allowedExtension, extension, fileName);
            }
        }

    }

    /**
     * 判断MIME类型是否是允许的MIME类型
     *
     * @param extension        上传文件类型
     * @param allowedExtension 允许上传文件类型
     * @return true/false
     */
    public static boolean isAllowedExtension(String extension, String[] allowedExtension) {
        for (String str : allowedExtension) {
            if (str.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }
}

你可能感兴趣的:(Java,secret,Java,secret)