【问题标题】:Google App Engine session changes between instances实例之间的 Google App Engine 会话更改
【发布时间】:2018-04-03 21:55:40
【问题描述】:

A visual aid to help you understand the issue.

我很困惑,因为我尝试启动的每个应用程序 100% 都会出现此问题,但我找不到有关此问题的任何信息,也找不到任何人互联网上遇到此问题的其他人。

我已经使用 Google Cloud Platform 学习使用 Java 进行 Web 开发大约一年了,但我有一个问题已经困扰了几个月。我似乎在网上的任何地方都找不到关于这个问题的任何提及。

我有一个基于 Java servlet 构建的 Web 应用程序,它使用会话来跟踪登录用户。应用程序中的一切工作正常,但是当我将应用程序部署到 App Engine 时,用户不断随机登录和退出后续服务器请求。我在开发窗口中观察,发现会话 ID 在两个不同的值之间不断变化。

因为应用引擎使用应用的多个实例来扩展流量和资源管理,它可能会在一个实例上接收请求,然后将其从另一个实例发送回客户端。这在客户端没有显着差异,但我发现每个实例对我的客户端都有不同的会话 ID,导致不同的会话属性被困在每个实例中。

果然,当我将应用程序减少到一个实例时,我的会话运行良好。但这在生产环境中行不通,我需要我的应用能够随流量扩展并按预期利用应用引擎资源。

Google 声称应用引擎上的会话是使用 Memcache 和 Datastore 自动处理的,访问该会话所需要做的就是使用 request.getSession(),所以我不明白为什么会出现这个问题。如果我无法理解问题,我将永远找不到解决方案:(

[编辑]: 该应用程序使用名为 DatastoreSessionFilter 的预先存在的 Web 过滤器来跟踪使用 cookie 和数据存储的会话变量,但它似乎无法正常工作,因为我的会话变量被困在单个实例中。这是 DatastoreSessionFilter 类:

@WebFilter(filterName = "DatastoreSessionFilter", 
    urlPatterns = { "", 
    "/LoginEmail", 
    "/Logout", 
    "/ResetPassword",
    "/SignupEmail", 
    "/VerifyEmail", 

    "/About", 
    "/BulkUpload", 
    "/DeleteAlbum", 
    "/DeletePost",
    "/EditAlbum", 
    "/EditPost",
    "/Events", 
    "/EventsScroll", 
    "/Home", 
    "/HomeScroll", 
    "/Info",
    "/ListAlbums",
    "/ListAlbumsScroll",
    "/NewAlbum",
    "/NewPost",
    "/Support",
    "/Videos",
    "/VideosScroll",
    "/ViewAlbum",
    "/ViewAlbumScroll",
    "/ViewPost",
    "/ViewProfile",
  })

public class DatastoreSessionFilter implements Filter {

  private static Datastore datastore;
  private static KeyFactory keyFactory;
  private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS");

  @Override
  public void init(FilterConfig config) throws ServletException {
    // initialize local copy of datastore session variables

    datastore = DatastoreOptions.getDefaultInstance().getService();
    keyFactory = datastore.newKeyFactory().setKind("SessionVariable");
    // Delete all sessions unmodified for over two days
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    Query<Entity> query = Query.newEntityQueryBuilder().setKind("SessionVariable")
        .setFilter(PropertyFilter.le("lastModified", dt.minusDays(2).toString(dtf))).build();
    QueryResults<Entity> resultList = datastore.run(query);
    while (resultList.hasNext()) {
      Entity stateEntity = resultList.next();
      datastore.delete(stateEntity.getKey());
    }
  }
  // [END init]

  @Override
  public void doFilter(ServletRequest servletReq, ServletResponse servletResp, FilterChain chain)
      throws IOException, ServletException {

    HttpServletRequest req = (HttpServletRequest) servletReq;
    HttpServletResponse resp = (HttpServletResponse) servletResp;

    // Check if the session cookie is there, if not there, make a session cookie using a unique
    // identifier.
    String sessionId = getCookieValue(req, "bookshelfSessionId");

    if (sessionId.equals("")) {
      String sessionNum = new BigInteger(130, new SecureRandom()).toString(32);
      Cookie session = new Cookie("bookshelfSessionId", sessionNum);
      session.setPath("/");
      resp.addCookie(session);
    }

    Map<String, String> datastoreMap = loadSessionVariables(req); // session variables for request

    chain.doFilter(servletReq, servletResp); // Allow the servlet to process request and response

    HttpSession session = req.getSession(); // Create session map
    Map<String, String> sessionMap = new HashMap<>();
    Enumeration<String> attrNames = session.getAttributeNames();

    while (attrNames.hasMoreElements()) {
      String attrName = attrNames.nextElement();
      String sessName = session.getAttribute(attrName).toString();
      sessionMap.put(attrName, sessName);
      // DEFAULT: sessionMap.put(attrName, (String) session.getAttribute(attrName));
    }

    // Create a diff between the new session variables and the existing session variables
    // to minimize datastore access
    MapDifference<String, String> diff = Maps.difference(sessionMap, datastoreMap);
    Map<String, String> setMap = diff.entriesOnlyOnLeft();
    Map<String, String> deleteMap = diff.entriesOnlyOnRight();

    // Apply the diff
    setSessionVariables(sessionId, setMap);
    deleteSessionVariables(sessionId, FluentIterable.from(deleteMap.keySet()).toArray(String.class));
  }

  @SuppressWarnings("unused")
  private String mapToString(Map<String, String> map) {
    StringBuffer names = new StringBuffer();

    for (String name : map.keySet()) {
      names.append(name + " ");
    }

    return names.toString();
  }

  @Override
  public void destroy() {
  }

  protected String getCookieValue(HttpServletRequest req, String cookieName) {
    Cookie[] cookies = req.getCookies();

    if (cookies != null) {
      for (Cookie cookie : cookies) {
        if (cookie.getName().equals(cookieName)) {
          return cookie.getValue();
        }
      }
    }

    return "";
  }

  // [START deleteSessionVariables]
  /**
   * Delete a value stored in the project's datastore.
   * @param sessionId Request from which the session is extracted.
   */
  protected void deleteSessionVariables(String sessionId, String... varNames) {
    if (sessionId.equals("")) {
      return;
    }

    Key key = keyFactory.newKey(sessionId);
    Transaction transaction = datastore.newTransaction();

    try {
      Entity stateEntity = transaction.get(key);

      if (stateEntity != null) {
        Entity.Builder builder = Entity.newBuilder(stateEntity);
        StringBuilder delNames = new StringBuilder();

        for (String varName : varNames) {
          delNames.append(varName + " ");
          builder = builder.remove(varName);
        }

        datastore.update(builder.build());
      }

    } finally {

      if (transaction.isActive()) {
        transaction.rollback();
      }

    }

  }
  // [END deleteSessionVariables]

  protected void deleteSessionWithValue(String varName, String varValue) {
    Transaction transaction = datastore.newTransaction();

    try {
      Query<Entity> query = Query.newEntityQueryBuilder().setKind("SessionVariable")
          .setFilter(PropertyFilter.eq(varName, varValue)).build();

      QueryResults<Entity> resultList = transaction.run(query);

      while (resultList.hasNext()) {
        Entity stateEntity = resultList.next();
        transaction.delete(stateEntity.getKey());
      }

      transaction.commit();

    } finally {

      if (transaction.isActive()) {
        transaction.rollback();
      }

    }

  }

  // [START setSessionVariables]
  /**
   * Stores the state value in each key-value pair in the project's datastore.
   * @param sessionId Request from which to extract session.
   * @param varName the name of the desired session variable
   * @param varValue the value of the desired session variable
   */
  protected void setSessionVariables(String sessionId, Map<String, String> setMap) {

    if (sessionId.equals("")) {
      return;
    }

    Key key = keyFactory.newKey(sessionId);
    Transaction transaction = datastore.newTransaction();
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    dt.toString(dtf);

    try {
      Entity stateEntity = transaction.get(key);
      Entity.Builder seBuilder;

      if (stateEntity == null) {
        seBuilder = Entity.newBuilder(key);
      } else {
        seBuilder = Entity.newBuilder(stateEntity);
      }

      for (String varName : setMap.keySet()) {
        seBuilder.set(varName, setMap.get(varName));
      }

      transaction.put(seBuilder.set("lastModified", dt.toString(dtf)).build());
      transaction.commit();

    } finally {

      if (transaction.isActive()) {
        transaction.rollback();
      }

    }

  }
  // [END setSessionVariables]

  // [START loadSessionVariables]
  /**
   * Take an HttpServletRequest, and copy all of the current session variables over to it
   * @param req Request from which to extract session.
   * @return a map of strings containing all the session variables loaded or an empty map.
   */
  protected Map<String, String> loadSessionVariables(HttpServletRequest req) throws ServletException {
    Map<String, String> datastoreMap = new HashMap<>();
    String sessionId = getCookieValue(req, "bookshelfSessionId");

    if (sessionId.equals("")) {
      return datastoreMap;
    }

    Key key = keyFactory.newKey(sessionId);
    Transaction transaction = datastore.newTransaction();

    try {
      Entity stateEntity = transaction.get(key);
      StringBuilder logNames = new StringBuilder();

      if (stateEntity != null) {

        for (String varName : stateEntity.getNames()) {
          req.getSession().setAttribute(varName, stateEntity.getString(varName));
          datastoreMap.put(varName, stateEntity.getString(varName));
          logNames.append(varName + " ");
        }

      }

    } finally {

      if (transaction.isActive()) {
        transaction.rollback();
      }

    }

    return datastoreMap;

  }
  // [END loadSessionVariables]
}

【问题讨论】:

  • 你实现了什么样的登录机制?自定义实现?
  • 您如何管理您的会话?预先存在的代码(如果是,是哪个),还是您自己编写的?
  • 我建议您将此信息存储在某个数据库中。仅将该信息存储在 mem-cache 或 session-cache 中,如果不存在,您可以从数据库中检索。
  • 感谢您的回复。我使用电子邮件和密码登录表单编写了一个自定义登录机制。它使用 JWT(Java Web 令牌)为用户会话创建令牌。至于会话管理
  • 关于会话管理,我使用的是 Google 编写的预先存在的网络过滤器 DatastoreSessionFilter.java。我相信它应该是负责使用数据存储和内存缓存存储/检索会话变量的类,但它似乎不起作用。我将编辑我的问题以包含 DatastoreSessionFilter 类。

标签: java google-app-engine servlets cookies httpsession


【解决方案1】:
According to Google App engine Documentation you need to define below configuration in appengine-web.xml 

 App Engine includes an implementation of sessions, using the servlet session interface. The implementation stores session data in the App Engine datastore for persistence, and also uses memcache for speed. As with most other servlet containers, the session attributes that are set with `session.setAttribute()` during the request are persisted at the end of the request. 


  <sessions-enabled>true</sessions-enabled>
  <async-session-persistence enabled="true" />

【讨论】:

  • 这就是我的想法,我在 appengine-web.xml 文件中设置了会话启用和异步会话持久性,并且我使用 servlet 会话接口来存储和检索会话数据,但我仍然遇到上述问题。
猜你喜欢
  • 1970-01-01
  • 2019-01-17
  • 2011-06-14
  • 2013-06-26
  • 1970-01-01
  • 1970-01-01
  • 2011-02-03
  • 1970-01-01
  • 2019-11-06
相关资源
最近更新 更多