Google App Engine for Java でBasic認証

Google App Engine for Javaは,Servletをベースとしている.Servletエンジンの多くには,Basic認証が組み込まれていて,設定ファイルを書くだけで使えるようなのだが,設定ファイルの書き方は,個々のServletエンジンに依存しているらしい.Google App EngineはいまのところJettyを使っている.なので,Jettyの流儀に従って設定ファイルを書けばひょっとしたらBasic認証を使えるのかもしれないのだが,いつJettyじゃなくなるかわからないのでちょっと怖くて使えない.ということで,Python版にひきつづき自前で実装してみた.なんか,こんなことをしなくてももっといい方法があったではないかという気がするが,これも勉強だということで,気にしない.

Filter

Servletにはフィルタチェインという概念がある.実ロジックのかかれているServletに到達する前に複数のフィルタを置くことができ,ここで,リクエストの変換や,認証を行うことができる.フィルタチェインは,web.xmlで定義することができるので,Servletの実装には一切手をいれることなく,認証の設定ができる.この流儀にしたがって,Basic認証をフィルタとして実装する.

使い方

web.xmlにフィルタを設定し,そのフィルタを特定のURLにマッピングすればよい.まずは,フィルタの定義.

  <filter>
    <filter-name>BasicAuth1</filter-name>
    <filter-class>BasicAuthFilter</filter-class>
    <init-param> 
      <param-name>realm</param-name>
      <param-value>REALM_NAME</param-value> 
    </init-param>
    <init-param> 
      <param-name>user.0</param-name>
      <param-value>USERNAME:PASSWD</param-value> 
    </init-param>
    <init-param> 
      <param-name>user.1</param-name>
      <param-value>USERNAME1:PASSWD1</param-value> 
    </init-param>
  }

realmの名前と,ユーザ名,パスワードのペアを初期化パラメータで指定する.'user.'で始まるパラメータはユーザの定義であると仮定してパーズする.区切りは':'.パスワードが平文というのもアレだが,まあ,そこはそれ.で,定義されたフィルタをURLにマップする,一つのフィルタを複数のURLにマップすることができる.

  <filter-mapping>
    <filter-name>BasicAuth1</filter-name>
    <url-pattern>/somewhere</url-pattern>
  </filter-mapping>

実装

動作はこんな感じ

  • 'Authorization'ヘッダが無ければ,401を返してbasic認証を要求
  • 'Authorization'ヘッダがあれば,中身を解析してユーザ辞書とつき合わせる.
    • 認証に成功したら,chain.doFilter を呼び出して,フィルタチェインの次のフィルタを呼び出す
    • 失敗したら401を返して再度認証を要求

base64のデコードに,apache のcommons.codecを使っている.appengine のパッケージの中にも含まれているっぽいのだけど,話がややこしくなるといやなので,別途jarを加えてある.

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.apache.commons.codec.binary.Base64;

public class BasicAuthFilter implements Filter{
  Map<String, String> userMap = new HashMap<String, String>();

  private static final Logger log = 
           Logger.getLogger(BasicAuthFilter.class.getName());

  private boolean tryAuth(String authHeader){
    if (authHeader == null)
      return false;
    String [] pair = authHeader.split(" ");
    if (pair.length != 2) {
      log.severe("failed to parse authHeader: " + authHeader);
      return false;
    }
    if (!pair[0].equals("Basic")) { //schema 
      log.severe("unsupported login scheme: " + pair[0]);
      return false;
    }
    String decoded = new String(Base64.decodeBase64(pair[1].getBytes()));
    String [] userPass = decoded.split(":");
    if (userPass.length != 2 || 
        !userMap.containsKey(userPass[0]) ||
        !userMap.get(userPass[0]).equals(userPass[1])) {
      log.severe("AuthFailure: " + decoded);
      return false;
    }
    log.info("authentication succeeded for user '" +  userPass[0]  + "'");
    return true; 
  }
  
  private void send401(ServletResponse response, 
                       String realm, String message) 
                       throws IOException{
    HttpServletResponse res = (HttpServletResponse)response;
    res.setStatus(401);
    res.setHeader("WWW-Authenticate", "Basic realm="+realm);
    res.getWriter().println("<body><h1>" + message + "</h1></body>\n");
    return;
  }
  
  String REALM = "Basic"; // default 
    
  public void doFilter(ServletRequest request,
                       ServletResponse response, 
                       FilterChain chain){
    try{
      HttpServletRequest req = (HttpServletRequest)request;
      String authHeader = req.getHeader("Authorization");
      if (! tryAuth(authHeader))  
        send401(response, REALM, "realm");
      else
        chain.doFilter(request, response);
    }catch (ServletException e){
      log.severe(e.getMessage());    
    }catch (IOException e){
      log.severe(e.getMessage());    
    }
  }

  public void init(FilterConfig filterConfig) 
                   throws ServletException{
    String tmp = filterConfig.getInitParameter("realm");
    if (tmp != null)
      REALM = tmp;
    log.info("realm = " + REALM);
    Enumeration keys = filterConfig.getInitParameterNames(); 
    while (keys.hasMoreElements()){ 
      String key = (String)keys.nextElement();
      if (key.startsWith("user.")) {  // assumes it is USER:PASS 
        String [] userPass = 
           filterConfig.getInitParameter(key).split(":");
        if (userPass.length == 2) {
          userMap.put(userPass[0], userPass[1]);
          log.info("new user :" + userPass[0]);
        }
      }
    }
  }

  public void destroy(){
  }
}

所感

ServletAPIが古いのには参った.FilterConfigのgetInitParameterNamesがEnumerationを返してくる...Enumeration なんか使ったの4,5年ぶりじゃないだろうか.