为了保护高安全级别的网络安全,国家保密局于1999年12月27日发布了涉密网络的物理隔离要求,并于2000年1月1日颁布实施的《计算机信息系统国际联网保密管理规定》,该规定中第二章保密制度第六条规定;“涉及国家秘密的计算机信息系统,不得直接或间接地与国际互联网或其余公共信息网络相链接,必须实行物理隔离。”
物理隔离一般是经过部署网闸来切断内网和外网的物理链接和逻辑链接,网闸只摆渡原始数据,而不允许任何链接或者协议通过网闸。在这种环境下,内外网邮件的传输成了难题。本文以探究的方式尝试提供一套思路和实现解决该场景下的邮件传输,本文包含的代码都是demo级别代码,秉着对新领域的探究,但不肯定该方法是否是合适该场景的解决方案。html
本文采用的环境以下:java
Apache James-2.3.2.1mysql
pom文件以下:github
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>MailInPIE</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.8</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.2.0</version> </dependency> </dependencies> </project>
大体的架构以下图:sql
因为重重困难,因此这个demo将跳过网闸部分,消息发送到消息队列以后,消费者直接将消息进行转发,虽然没有实际在网闸和内外网的环境下测试,可是!我以为是可行的!数据库
mailet程序片断的实现思路以下:apache
DivestitureAgreementMatcher参考代码:json
package com.xxxxx.pie.mail.matcher; import com.jlszkxa.pie.mail.db.DbOperation; import org.apache.mailet.GenericRecipientMatcher; import org.apache.mailet.MailAddress; import java.sql.SQLException; /** * @ClassName DivestitureAgreementMatcher * @Description 判断接收人是不是内网用户,是则匹配给Mailet进行原始内容抽取,不然放行 * @Author chenwj * @Date 2020/2/20 14:53 * @Version 1.0 **/ public class DivestitureAgreementMatcher extends GenericRecipientMatcher { @Override public boolean matchRecipient(MailAddress mailAddress) { DbOperation dbOperation = new DbOperation(); try { dbOperation.connectDatabase(); String userName = mailAddress.getUser(); String host = mailAddress.getHost(); System.out.printf("截取到到发送给%s@%s的邮件\r\n", userName, host); return !dbOperation.isInnerUser(userName + "@" + host); } catch (Exception e) { System.out.printf("发生异常 异常信息: %s\r\n", e.getMessage()); e.printStackTrace(); return true; } finally { try { dbOperation.closeConnection(); } catch (SQLException e) { e.printStackTrace(); } } } }
dbOperation参考代码:api
package com.xxxxxx.pie.mail.db; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; /** * @ClassName DbOperation * @Description 数据库操做 * @Author chenwj * @Date 2020/2/20 17:34 * @Version 1.0 **/ public class DbOperation { private static final Logger logger = LoggerFactory.getLogger(DbOperation.class); private Connection connection; /* 链接数据库 */ public void connectDatabase() { String driver = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/mail?characterEncoding=UTF-8"; String userName = "root"; String password = "123456"; logger.info("开始链接数据库"); try { Class.forName(driver); connection = DriverManager.getConnection(url, userName, password); logger.info("数据库链接成功"); } catch (Exception e) { logger.warn("数据库链接出现异常 异常信息: {}", e.getMessage()); } } /** * 判断该用户是否为内网用户 * * @param userName * @return * @throws Exception */ public boolean isInnerUser(String userName) throws Exception { String sql = "select USER_NAME from james_user where USER_NAME = \'" + userName + "\';"; PreparedStatement pstmt = connection.prepareStatement(sql); ResultSet rs = pstmt.executeQuery(); try { if (rs.next()) { return true; } return false; } finally { rs.close(); pstmt.close(); } } /* 关闭链接 */ public void closeConnection() throws SQLException { if (null != connection) { connection.close(); } } public static void main(String[] args) throws Exception { DbOperation test = new DbOperation(); test.connectDatabase(); boolean innerUser = test.isInnerUser("97983398@qq.com"); System.out.printf("结果为: %s\r\n", "true"); test.closeConnection(); } }
本例中搭建的james邮件服务器将用户信息存储在数据库中,所以能够直接经过查询数据库的手段判断是不是内网用户。此外,也能够直接截取域名进行判断。
DivestitureAgreementMailet参考代码:
package com.xxxxxx.pie.mail.mailet; import com.alibaba.fastjson.JSONObject; import com.jlszkxa.pie.mail.entity.ForwardMail; import org.apache.mailet.GenericMailet; import org.apache.mailet.Mail; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import javax.mail.MessagingException; import java.io.IOException; /** * @author chenwj * @version 1.0 * @className DivestitureAgreementMailet * @description 截取邮件,剥离协议,并放入队列等待网闸摆渡 * @date 2020/2/20 14:53 **/ public class DivestitureAgreementMailet extends GenericMailet { @Override public void service(Mail mail) throws MessagingException { String sender = mail.getSender().toString(); String name = mail.getName(); String subject = mail.getMessage().getSubject(); String content = null; try { content = (String) mail.getMessage().getContent(); } catch (IOException e) { e.printStackTrace(); } System.out.printf("截取到%s的邮件 name:%s subject:%s content:%s\r\n", sender, name, subject, content); System.out.println("将截取邮件发送到消息队列中..."); DefaultMQProducer producer = new DefaultMQProducer("DivestitureAgreementGroup"); producer.setNamesrvAddr("localhost:9876"); producer.setInstanceName("rmq-instance"); try { producer.start(); System.out.println("开启消息队列"); } catch (MQClientException e) { e.printStackTrace(); } try { ForwardMail forwardMail = ForwardMail.newBuilder() .content(mail.getMessage().getContent()) .from(mail.getSender().getUser() + "@" + mail.getSender().getHost()) .hostName(mail.getRemoteHost()) .recipients(mail.getRecipients().iterator().next()) .subject(mail.getMessage().getSubject()) .build(); Message message = new Message("demo-topic", "demo-tag", JSONObject.toJSONString(forwardMail).getBytes("UTF-8")); producer.send(message); System.out.println("消息成功转发到消息队列中"); System.out.printf("转发内容以下: %s\r\n", JSONObject.toJSONString(forwardMail)); } catch (MQClientException e) { e.printStackTrace(); } catch (RemotingException e) { e.printStackTrace(); } catch (MQBrokerException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ producer.shutdown(); System.out.println("关闭消息队列"); } } }
ForwardMail对象以下:
package com.xxxxxx.pie.mail.entity; /** * @ClassName Mail * @Description TODO * @Author chenwj * @Date 2020/2/21 13:42 * @Version 1.0 **/ public class ForwardMail { private String hostName; private String from; private String subject; private Object recipients; private Object content; public ForwardMail() { } private ForwardMail(Builder builder) { setHostName(builder.hostName); setFrom(builder.from); setSubject(builder.subject); setRecipients(builder.recipients); setContent(builder.content); } public static Builder newBuilder() { return new Builder(); } public String getHostName() { return hostName; } public void setHostName(String hostName) { this.hostName = hostName; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public Object getRecipients() { return recipients; } public void setRecipients(Object recipients) { this.recipients = recipients; } public Object getContent() { return content; } public void setContent(Object content) { this.content = content; } public static final class Builder { private String hostName; private String from; private String subject; private Object recipients; private Object content; private Builder() { } public Builder hostName(String val) { hostName = val; return this; } public Builder from(String val) { from = val; return this; } public Builder subject(String val) { subject = val; return this; } public Builder recipients(Object val) { recipients = val; return this; } public Builder content(Object val) { content = val; return this; } public ForwardMail build() { return new ForwardMail(this); } } }
将上述的mailet程序片断打成jar包,粘贴复制到james服务器下..\james-2.3.2.1\apps\james\SAR-INF\lib
,若是SAR-INF目录下没有lib目录,则手动新增该目录,在..\james-2.3.2.1\apps\james\SAR-INF\config.xml
中增长以下配置:
重启james服务器,mailet程序片断就能够生效了。下面是消费端消息的转发。
邮件的转发实际上是我考虑比较久的,内网与外网没法创建链接的状况下,要将消息原封不动地进行转发,且消息发送人依旧标识是内网的用户,这是我指望实现的,可是我没有找到好的解决办法。我尝试将邮件的from设置为内网用户,或者将displayname设置为内网用户,在QQ邮箱中彷佛都没有获得比较好的结果。所以最后我取巧实现,在外网统一由一个外网用户进行转发,转发时在subject中标识该邮件转发自内网的某个用户。
Consumer参考代码:
package com.xxxxxx.pie.mail.mq; import com.alibaba.fastjson.JSONObject; import com.jlszkxa.pie.mail.entity.ForwardMail; import com.jlszkxa.pie.mail.mail.MailSender; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import javax.mail.MessagingException; import java.io.UnsupportedEncodingException; import java.util.List; public class Consumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-group"); consumer.setNamesrvAddr("localhost:9876"); consumer.setInstanceName("rmq-instance"); consumer.subscribe("demo-topic", "demo-tag"); consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage( List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { ForwardMail forwardMail = JSONObject.parseObject(new String(msg.getBody()), ForwardMail.class); JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(forwardMail.getRecipients())); String recipient = jsonObject.getString("user") + "@" + jsonObject.getString("host"); try { System.out.printf("成功代理转发%s的邮件\r\n", recipient); MailSender.sendHtml(forwardMail.getFrom(), "979831398@qq.com", "xxxxx", "smtp.qq.com", recipient, "转发自代理服务器由" + forwardMail.getFrom().split("@")[0] + "发出的邮件:" + forwardMail.getSubject(), forwardMail.getContent()); } catch (MessagingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.println("Consumer Started."); } }
MailSender参考代码:
package com.xxxxxx.pie.mail.mail; import javax.mail.MessagingException; import javax.mail.internet.AddressException; import java.io.UnsupportedEncodingException; /** * @author * */ public class MailSender { /** * 服务邮箱 */ private static MailServer mailServer = null; // private static String userName; private static String password; private static String stmp; /** * @param userName the userName to set */ public void setUserName(String userName) { if(MailSender.userName==null) MailSender.userName = userName; } /** * @param password the password to set */ public void setPassword(String password) { if(MailSender.password==null) MailSender.password = password; } /** * @param stmp the stmp to set */ public void setStmp(String stmp) { if(MailSender.stmp==null) MailSender.stmp = stmp; } /** * 使用默认的用户名和密码发送邮件 * @param recipient * @param subject * @param content * @throws MessagingException * @throws AddressException */ public static void sendHtml(String recipient, String subject, Object content, String fromname) throws AddressException, MessagingException, UnsupportedEncodingException { if (mailServer == null) mailServer = new MailServer(stmp,userName,password); mailServer.send(recipient, subject, content, fromname); } /** * 使用指定的用户名和密码发送邮件 * @param server * @param password * @param recipient * @param subject * @param content * @throws MessagingException * @throws AddressException */ public static void sendHtml(String fromname, String server,String password,String stmpIp, String recipient, String subject, Object content) throws AddressException, MessagingException, UnsupportedEncodingException { new MailServer(stmpIp,server,password).send(recipient, subject, content, fromname); } public static void main(String[] args) { try { String s = "这是一封来自公司内网的测试邮件,收到请勿回复!"; sendHtml(null, "test@xxxxx.com","test","localhost", "979831398@qq.com", "测试邮件", s); } catch (AddressException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (MessagingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }
MailServer参考代码:
package com.xxxxxx.pie.mail.mail; import org.apache.commons.lang3.StringUtils; import javax.mail.*; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Properties; /** * 简单邮件发送器,可单发,群发。 * * @author humingfeng * */ public class MailServer { /** * 发送邮件的props文件 */ private final transient Properties props = System.getProperties(); /** * 邮件服务器登陆验证 */ private transient MailAuthenticator authenticator; /** * 邮箱session */ private transient Session session; /** * 初始化邮件发送器 * * @param smtpHostName * SMTP邮件服务器地址 * @param username * 发送邮件的用户名(地址) * @param password * 发送邮件的密码 */ public MailServer(final String smtpHostName, final String username, final String password) { init(username, password, smtpHostName); } /** * 初始化邮件发送器 * * @param username * 发送邮件的用户名(地址),并以此解析SMTP服务器地址 * @param password * 发送邮件的密码 */ public MailServer(final String username, final String password) { // 经过邮箱地址解析出smtp服务器,对大多数邮箱都管用 final String smtpHostName = "smtp." + username.split("@")[1]; init(username, password, smtpHostName); } /** * 初始化 * * @param username * 发送邮件的用户名(地址) * @param password * 密码 * @param smtpHostName * SMTP主机地址 */ private void init(String username, String password, String smtpHostName) { // 初始化props props.put("mail.smtp.auth", "true"); props.put("mail.smtp.host", smtpHostName); if(smtpHostName==null)props.put("mail.smtp.host", smtpHostName); // 验证 authenticator = new MailAuthenticator(username, password); // 建立session session = Session.getInstance(props, authenticator); } /** * 发送邮件 * * @param recipient * 收件人邮箱地址 * @param subject * 邮件主题 * @param content * 邮件内容 * @throws AddressException * @throws MessagingException */ public void send(String recipient, String subject, Object content, String fromname) throws AddressException, MessagingException, UnsupportedEncodingException { // 建立mime类型邮件 final MimeMessage message = new MimeMessage(session); // 设置发信人 if(StringUtils.isBlank(fromname)) { message.setFrom(new InternetAddress(authenticator.username, fromname)); } else { message.setFrom(new InternetAddress(authenticator.username)); } // 设置收件人 if(recipient!=null&&recipient.indexOf(";")!=-1){ //多收件人 String[] rec = recipient.split(";"); int len = rec.length; InternetAddress[] iad = new InternetAddress[len]; for(int i=0; i<len; i++){ iad[i] = new InternetAddress(rec[i]); } message.setRecipients(MimeMessage.RecipientType.TO, iad); }else{ //单收件人 message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(recipient)); } // 设置主题 message.setSubject(subject); // 设置邮件内容 message.setContent(content.toString(), "text/html;charset=utf-8"); // message.setText(content.toString(), "GBK"); // 发送 Transport.send(message); } /** * 群发邮件 * * @param recipients * 收件人们 * @param subject * 主题 * @param content * 内容 * @throws AddressException * @throws MessagingException */ public void send(List<String> recipients, String subject, Object content, String fromname) throws AddressException, MessagingException, UnsupportedEncodingException { // 建立mime类型邮件 final MimeMessage message = new MimeMessage(session); // 设置发信人 if(StringUtils.isBlank(fromname)) { message.setFrom(new InternetAddress(authenticator.username, fromname)); } else { message.setFrom(new InternetAddress(authenticator.username)); } // 设置收件人们 final int num = recipients.size(); InternetAddress[] addresses = new InternetAddress[num]; for (int i = 0; i < num; i++) { addresses[i] = new InternetAddress(recipients.get(i)); } message.setRecipients(MimeMessage.RecipientType.TO, addresses); // 设置主题 message.setSubject(subject); // 设置邮件内容 message.setContent(content.toString(), "text/html;charset=utf-8"); // 发送 Transport.send(message); } /** * 服务器邮箱登陆验证 * * @author MZULE * */ public class MailAuthenticator extends Authenticator { /** * 用户名(登陆邮箱) */ private String username; /** * 密码 */ private String password; /** * 初始化邮箱和密码 * * @param username * 邮箱 * @param password * 密码 */ public MailAuthenticator(String username, String password) { this.username = username; this.password = password; } String getPassword() { return password; } @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } String getUsername() { return username; } public void setPassword(String password) { this.password = password; } public void setUsername(String username) { this.username = username; } } }
最后启动james服务和Comsumer服务,测试邮件转发结果以下:
经过某一对外用户转发的方式转发到外网的邮件难免存在一个问题,抵赖。某个用户明明发送了邮件,却抵赖本身不曾发过。关于这点我考虑能够经过数字签名的方式解决,某个用户在内网建立时生成公私钥对,经过对邮件内容签名的方式,保证邮件的不可篡改性和不可抵赖性。
最后,上面全部代码均已上传至github仓库。
上面的思路和实现仅仅是我在这个新领域的摸索,可能与实际落地的方案相差甚远,但也算是我对这个新领域的一次微弱的攻击。之后但愿能在数据交换领域拓宽个人视野,结识更多在这个领域中兢兢业业的大佬们,为个人许多迷茫指引方向。