Используя javamail, gmail отказывается от аутентификации из-за менее безопасного приложения

У меня запущена очень простая программа Javamail, чтобы попробовать отправлять электронные письма. Это автономная программа с main(). Как только я его заработаю, я планирую использовать Javamail в сервлете, работающем под tomcat.

При запуске этой программы я получал ошибку AUTH LOGIN. Я попробовал несколько разных настроек свойств, ни одна из которых не решила проблему.

Затем я нашел сообщение о SO, которое предложило снизить необходимый уровень безопасности в моей учетной записи Google. Аутентификация прошла успешно, когда я опустил настройку безопасности.

Конечно, я сразу вернулся к более высокому уровню безопасности в учетной записи Google.

У меня есть вопрос: как я могу сделать свое приложение более безопасным, чтобы gmail не отказывался от аутентификации?

Программный код, показанный ниже. Программа очень похожа на код во многих других вопросах Javamail на SO.

TryJavamail.java

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Properties;
import javax.mail.*;
import javax.mail.internet.*;

public class TryJavamail {

  public static void main(String args[]) throws MessagingException {

    String submitName = "John Doe";
    String submitEmail = "[email protected]";
    String submitMessage = "This is the message";

    Properties props = new Properties();
    props.put("mail.transport.protocol", "smtp");
    props.setProperty("mail.smtp.host", "smtp.gmail.com");
    props.setProperty("mail.smtp.auth", "true");
    props.setProperty("mail.smtp.ssl.enable", "true");
    props.setProperty("mail.smtp.port", "465");
    Session session = Session.getInstance(props, null);

    session.setDebug(true);

    Message message = new MimeMessage(session);
    message.setSubject("Message from myapp website submit");
    message.setText(submitName + "; " + submitMessage);

    Address toAddress = new InternetAddress(submitEmail);
    message.setRecipient(Message.RecipientType.TO, toAddress);

    Transport transport = session.getTransport("smtp");
    transport.connect("smtp.gmail.com", "---userid---", "---password---");
    transport.sendMessage(message, message.getAllRecipients());
    transport.close();
  }
}

Ответ 2

Я включаю свое решение в качестве отдельного ответа. Я ранее редактировал вопрос, чтобы включить это, но вопрос стал слишком длинным.

Сервлет с использованием проверки подлинности OAuth2 ниже

Ниже показан сервлет, который использует OAuth2 для отправки электронной почты из формы "Связаться" на моем веб-сайте. Я выполнил инструкции, приведенные в ссылке, предоставленной Биллом.

SendMessage.java(требуется более сложная реализация, пожалуйста, прочитайте комментарии в коде)

/*
 * This program is adapted from sample code provided
 * by Google Inc at the following location:
 *          https://github.com/google/gmail-oauth2-tools
 *
 */

package com.somedomain.servlet;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Properties;
import java.util.logging.Logger;

import javax.mail.Session;
import javax.mail.Message;
import javax.mail.Address;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;

import java.security.Provider;
import java.security.Security;

import com.sun.mail.smtp.SMTPTransport;

import com.somedomain.oauth2.AccessTokenFromRefreshToken;
import com.somedomain.oauth2.OAuth2SaslClientFactory;

public class SendMessage extends HttpServlet {

    private static final Logger logger =
        Logger.getLogger(SendMessage.class.getName());

    public static final class OAuth2Provider extends Provider {
        private static final long serialVersionUIS = 1L;

        public OAuth2Provider() {
            super("Google OAuth2 Provider", 1.0,
                  "Provides the XOAUTH2 SASL Mechanism");
            put("SaslClientFactory.XOAUTH2",
                    "com.somedomain.oauth2.OAuth2SaslClientFactory");
        }
    }

    public static void initialize() {
        Security.addProvider(new OAuth2Provider());
    }

    public static SMTPTransport connectToSmtp(Session session,
                                            String host,
                                            int port,
                                            String userEmail,
                                            String oauthToken,
                                            boolean debug) throws Exception {

        final URLName unusedUrlName = null;
        SMTPTransport transport = new SMTPTransport(session, unusedUrlName);
        // If the password is non-null, SMTP tries to do AUTH LOGIN.
        final String emptyPassword = "";
        transport.connect(host, port, userEmail, emptyPassword);

        return transport;
    }

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
                          throws ServletException, IOException
    {
        String submitName = request.getParameter("name");
        String submitEmail = request.getParameter("email");
        String submitPhone = request.getParameter("phone");
        String submitMessage = request.getParameter("message");

        try {

            String host = "smtp.gmail.com";
            int    port = 587;
            String userEmail = "---email account used for oauth2---";
            String appEmail = "---email account for receiving app emails---";
            String oauthToken = "";

            initialize();

            //
            // Gmail access tokens are valid for 1 hour. A more sophisticated
            // implementation would store access token somewhere and reuse it
            // if it was not expired. A new access token should be generated
            // only if access token is expired. Abandoning unexpired access
            // tokens seems wasteful.
            //
            oauthToken = AccessTokenFromRefreshToken.getAccessToken();
            Properties props = new Properties();
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.starttls.required", "true");
            props.put("mail.smtp.sasl.enable", "true");
            props.put("mail.smtp.sasl.mechanisms", "XOAUTH2");
            props.put(OAuth2SaslClientFactory.OAUTH_TOKEN_PROP, oauthToken);

            Session session = Session.getInstance(props);
            session.setDebug(true);

            SMTPTransport smtpTransport = connectToSmtp(session, host, port,
                                                userEmail, oauthToken, true);

            Message message = new MimeMessage(session);
            message.setSubject("Submit from somedomain.com website");
            message.setText("Name=" + submitName + "\n\nEmail=" + submitEmail +
                    "\n\nPhone=" + submitPhone + "\n\nMessage=" + submitMessage);


            Address toAddress = new InternetAddress(appEmail);
            message.setRecipient(Message.RecipientType.TO, toAddress);

            smtpTransport.sendMessage(message, message.getAllRecipients());
            smtpTransport.close();
        } catch (MessagingException e) {
            System.out.println("Messaging Exception");
            System.out.println("Error: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("Messaging Exception");
            System.out.println("Error: " + e.getMessage());
        }

        String url = "/thankyou.html";
        response.sendRedirect(request.getContextPath() + url);
    }
}

AccessTokenFromRefreshToken.java

/*
 * For OAuth2 authentication, this program generates
 * access token from a previously acquired refresh token.
 */

package com.somedomain.oauth2;

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.LinkedHashMap;
import java.io.DataOutputStream;
import java.io.Reader;
import java.io.BufferedReader;
import java.io.InputStreamReader;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;


public class AccessTokenFromRefreshToken {

    public static String getAccessToken() {

        HttpURLConnection conn = null;
        String accessToken = null;

        try {

            URL url = new URL("https://accounts.google.com/o/oauth2/token");

            Map<String,Object> params = new LinkedHashMap<>();
            params.put("client_id", "***********.apps.googleusercontent.com");
            params.put("client_secret", "****************");
            params.put("refresh_token", "*****************");
            params.put("grant_type", "refresh_token");

            StringBuilder postData = new StringBuilder();
            for (Map.Entry<String,Object> param : params.entrySet()) {
                if (postData.length() != 0) postData.append('&');
                postData.append(URLEncoder.encode(param.getKey(), "UTF-8"));
                postData.append('=');
                postData.append(URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8"));
            }
            byte[] postDataBytes = postData.toString().getBytes("UTF-8");

            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            conn.setRequestProperty("Content-Length",
                                String.valueOf(postDataBytes.length));
            conn.setRequestProperty("Content-language", "en-US");
            conn.setDoOutput(true);

            DataOutputStream wr = new DataOutputStream (
                            conn.getOutputStream());
            wr.write(postDataBytes);
            wr.close();

            StringBuilder sb = new StringBuilder();
            Reader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            for ( int c = in.read(); c != -1; c = in.read() ) {
                sb.append((char)c);
            }

            String respString = sb.toString();

            // Read access token from json response
            ObjectMapper mapper = new ObjectMapper();
            AccessTokenObject accessTokenObj = mapper.readValue(respString,
                                                        AccessTokenObject.class);
            accessToken = accessTokenObj.getAccessToken();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(conn != null) {
                conn.disconnect(); 
            }
        }

        return(accessToken);
    }
}

AccessTokenObject.java

/*
 * Class that corresponds to the JSON
 * returned by google OAuth2 token generator
 */

package com.somedomain.oauth2;

import com.fasterxml.jackson.annotation.JsonProperty;

public class AccessTokenObject {
    @JsonProperty("access_token")
    private String accessToken;

    @JsonProperty("token_type")
    private String tokenType;

    @JsonProperty("expires_in")
    private int expiresIn;

    public String getAccessToken() { return accessToken; }
    public String getTokenType() { return tokenType; }
    public int getExpiresIn() { return expiresIn; }

    public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
    public void setTokenType(String tokenType) { this.tokenType = tokenType; }
    public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; }
}

OAuth2SaslClient.java. Код используется без изменений из gmail-oauth2-tools, за исключением того, что оператор пакета добавлен сверху (package com.somedomain.oauth2;)

OAuth2SaslClientFactory.java - код используется без изменений, добавлена ​​заявка пакета

Ответ 3

как сделать мое приложение более безопасным, чтобы gmail не отказывать в аутентификации?


Одним из хороших способов, на мой взгляд, было бы включить двустороннюю аутентификацию и заменить обычный пароль Gmail на сгенерированный пароль приложения в вашем коде.

final String smtpServer = "smtp.gmail.com";
final String userAccount = "****@gmail.com"; // Sender Account.
final String password = "****"; // Password -> Application Specific Password.
final String SOCKET_FACTORY = "javax.net.ssl.SSLSocketFactory";
final String smtpPort = "587";
final String PORT = "465";

final Properties props = new Properties();
props.put("mail.smtp.host", smtpServer);
props.put("mail.smtp.user", userAccount);
props.put("mail.smtp.password", password);
props.put("mail.smtp.port", smtpPort);
props.put("mail.smtp.auth", true);
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.debug", "false");
props.put("mail.smtp.socketFactory.port", PORT);
props.put("mail.smtp.socketFactory.class", SOCKET_FACTORY);
props.put("mail.smtp.socketFactory.fallback", "false");

Session session = Session.getInstance(props,
new javax.mail.Authenticator() {
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(userAccount, password);
    }
});
MimeMessage mimeMessage = new MimeMessage(session);
final Address toAddress = new InternetAddress("****@outlook.com"); // toAddress
final Address fromAddress = new InternetAddress(userAccount);
mimeMessage.setContent("This is a test mail...", "text/html; charset=UTF-8");
mimeMessage.setFrom(fromAddress);
mimeMessage.setRecipient(javax.mail.Message.RecipientType.TO, toAddress);
mimeMessage.setSubject("Test Mail...");
Transport transport = session.getTransport("smtp");
transport.connect(smtpServer, userAccount, password);
transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());