使用javamail,gmail由于应用程序安全性较低而拒绝身份验证

2022-09-02 10:43:52

我正在运行一个非常基本的Javamail程序来尝试发送电子邮件。这是带有 main() 的独立程序。一旦我让它工作,我计划在tomcat下运行的servlet中使用Javamail。

运行此程序时,我收到AUTH登录失败错误。我尝试了几种不同的属性设置,但没有一个解决问题。

然后我发现了一个关于SO的帖子,建议降低我的Google帐户中所需的安全级别。当我降低安全设置时,身份验证成功。

当然,我立即在Google帐户上返回到更高的安全级别。

我的问题是,如何使我的应用程序更安全,以便gmail不会拒绝身份验证?

程序代码如下所示。该程序与SO上许多其他Javamail问题中的代码非常相似。

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 = "from@example.com";
    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();
  }
}

答案 1

您可能希望使用 OAuth2 身份验证


答案 2

我将我的解决方案作为单独的答案包括在内。我之前编辑过这个问题以包括这一点,但问题变得太长了。

下面使用 OAuth2 身份验证的 Servlet

下面显示的是一个 servlet,它使用 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 - 未更改使用的代码,添加了包语句