【メモ】DropwizardでCookieのsession idで認証してみる

  • 11
    Like
  • 0
    Comment
More than 1 year has passed since last update.

やってること

せっかくfreemarkerとか梱包されてるので、普通のWEBアプリケーションを作ってみる上で、認証をSessionIDでやってみる。
そしてきっと探せば自作しなくてもすでにあるのかもしれない。

/* TODO コードとか作りとか色々汚いので使うときにはちゃんと修正する */
  • Cookieにsidが設定されていない場合はログイン画面に飛ばす
  • ログイン時にCookieにsidを設定してあげる

コード

とりあえず書いてみた適当なコード

jp.hoge.app.auth.SessionUserAuthFactory.java
package jp.hoge.app.auth;

import java.util.Arrays;
import java.util.Optional;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;

import io.dropwizard.auth.AuthFactory;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.UnauthorizedHandler;

/**
 * ほぼほぼio.dropwizard.auth.basic.BasicAuthFactoryの写経でいけた
 */
public class SessionUserAuthFactory<T> extends
        AuthFactory<HogeTokenCredentials, T> {

    @Context
    private HttpServletRequest request;
    private final boolean required;
    private UnauthorizedHandler unauthorizedHandler = new HogeUnauthorizedHandler();
    private final Class<T> generatedClass;
    private String prefix = "Sid"; // 使わないけど
    private final String realm; // 使わないけど

    public SessionUserAuthFactory(
            Authenticator<HogeTokenCredentials, T> authenticator,
            final String realm,
            final Class<T> generatedClass) {
        super(authenticator);
        this.required = false;
        this.realm = realm;
        this.generatedClass = generatedClass;
    }
    private SessionUserAuthFactory(final boolean required,
            final Authenticator<HogeTokenCredentials, T> authenticator,
            final String realm,
            final Class<T> generatedClass) {
        super(authenticator);
        this.required = required;
        this.realm = realm;
        this.generatedClass = generatedClass;
    }

    public SessionUserAuthFactory<T> responseBuilder(UnauthorizedHandler unauthorizedHandler) {
        this.unauthorizedHandler = unauthorizedHandler;
        return this;
    }

    @Override
    public T provide() {
        // ラムダ式で書いてみたかったの (正しいのか知らん)
        Optional<HogeTokenCredentials> credentials = Optional.ofNullable(request)
                .map(req -> req.getCookies())
                .map(cookies -> Arrays.asList(cookies).stream()
                        .filter(cookie -> "sid".equals(cookie.getName()))
                        .findFirst().orElseGet(null))
                .map(sidcookie -> sidcookie.getValue())
                .map(sid -> new HogeTokenCredentials(sid));

        if (credentials.isPresent()) {
            try {
                // guava出てきてもうた
                final com.google.common.base.Optional<T> result = authenticator().authenticate(credentials.get());
                if (result.isPresent()) return result.get();
            } catch (AuthenticationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } 
        // 認証必須のリソースの場合はUnauthorizedHandlerを利用してログイン画面に飛ばす
        if (required) {
            throw new WebApplicationException(unauthorizedHandler.buildResponse(prefix, realm));
        }

        return null;
    }

    @Override
    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public AuthFactory<HogeTokenCredentials, T> clone(boolean required) {
        return new SessionUserAuthFactory<>(required, authenticator(),this.realm, this.generatedClass).responseBuilder(unauthorizedHandler);
    }

    @Override
    public Class<T> getGeneratedClass() {
        return generatedClass;
    }

}

jp.hoge.app.auth.HogeUnauthorizedHandler.java
package jp.hoge.app.auth;

import java.net.URI;
import java.net.URISyntaxException;

import javax.ws.rs.core.Response;

import io.dropwizard.auth.UnauthorizedHandler;

public class HogeUnauthorizedHandler implements UnauthorizedHandler {

    @Override
    public Response buildResponse(String prefix, String realm) {

        try {
            // ログイン画面にリダイレクト
            return Response.seeOther(new URI("/login")).build();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return Response.ok().build();
    }
}

jp.hoge.app.auth.HogeTokenCredentials.java
package jp.hoge.app.auth;

public class HogeTokenCredentials {

    private final String token;
    public HogeTokenCredentials(String token) {
        this.token = token;
    }
    public String getToken() {
        return token;
    }
}

jp.hoge.app.auth.HogeSessionAuthenticator.java
package jp.hoge.app.auth;

import com.google.common.base.Optional;

import jp.hoge.app.core.HogeUser;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;

public class HogeSessionAuthenticator implements Authenticator<HogeTokenCredentials, HogeUser> {

    @Override
    public Optional<HogeUser> authenticate(HogeTokenCredentials credentials)
            throws AuthenticationException {
        // 本来はredisとかに問い合わせて有効か確認してユーザ情報を取得する
        if ("abcdefg".equals(credentials.getToken())) {
            return Optional.of(new HogeUser("myID"));
        }

        return Optional.absent();
    }

}

jp.hoge.app.core.HogeUser.java
package jp.hoge.app.core;

public class HogeUser {

    private final String userId;

    public HogeUser(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }

}

jp.hoge.app.resources.LoginResource.java
package jp.hoge.app.resources;


import java.net.URI;
import java.net.URISyntaxException;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;

import jp.hoge.app.views.LoginView;

@Path("/login")
public class LoginResource {

    @GET
    @Path("/")
    @Produces(MediaType.TEXT_HTML)
    public LoginView getLoginView() {
        return new LoginView(LoginView.Template.FREEMARKER);
    }

    @POST
    @Path("/signin")
    public Response signup(@FormParam("email") String email , @FormParam("password") String password) {
        // アイパス認証
        if ("test".equals(email) && "test".equals(password)) {
            // Cookie生成 (これだとブラウザ閉じるまでの間だけ)
            NewCookie cookies = new NewCookie("sid","abcdefg","/",null,"",NewCookie.DEFAULT_MAX_AGE,false);;
            try {
                // Cookieセットしてマイページ画面へリダイレクト
                return Response.seeOther(new URI("/mypage")).cookie(cookies).build();
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }
        }

        try {
            // 認証失敗したらログイン画面へ戻る
            return Response.seeOther(new URI("/login")).build();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return Response.serverError().build();
    }
}
jp.hoge.app.resources.HogeUserResource.java
package jp.hoge.app.resources;

import io.dropwizard.auth.Auth;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import jp.hoge.app.core.HogeUser;
import jp.hoge.app.views.MypageTopView;

@Path("/mypage")
public class HogeUserResource {

    @GET
    @Path("/")
    @Produces(MediaType.TEXT_HTML)
    public MypageTopView getMypageView(@Auth HogeUser user) {
        // @AuthでCookie認証後にUserが取得される
        return new MypageTopView(MypageTopView.Template.FREEMARKER,user);
    }
}

jp.hoge.app.views.LoginView.java
package jp.hoge.app.views;


import io.dropwizard.views.View;

public class LoginView extends View {

    public enum Template {
        FREEMARKER("login.ftl");

        private String templateName;
        private Template(String templateName){
            this.templateName = templateName;
        }

        public String getTemplateName(){
            return templateName;
        }
    }

    public LoginView(LoginView.Template templateName) {
        super(templateName.getTemplateName());
    }

}
jp.hoge.app.views.MypageTopView.java
package jp.hoge.app.views;

import jp.hoge.app.core.HogeUser;
import io.dropwizard.views.View;

public class MypageTopView extends View {

    private final HogeUser user;
    public enum Template {
        FREEMARKER("mypage_top.ftl");

        private String templateName;
        private Template(String templateName){
            this.templateName = templateName;
        }

        public String getTemplateName(){
            return templateName;
        }
    }

    public MypageTopView(MypageTopView.Template templateName,HogeUser user) {
        super(templateName.getTemplateName());
        this.user = user;
    }

    public HogeUser getUser() {
        return user;
    }

}

jp.hoge.app.HogeApplication.java
package jp.hoge.app;

import jp.hoge.app.auth.HogeSessionAuthenticator;
import jp.hoge.app.auth.SessionUserAuthFactory;
import jp.hoge.app.core.HogeUser;
import jp.hoge.app.resources.LoginResource;
import jp.hoge.app.resources.HogeUserResource;

import com.google.common.collect.ImmutableMap;

import io.dropwizard.Application;
import io.dropwizard.auth.AuthFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.views.ViewBundle;

public class HogeApplication extends Application<HogeConfiguration> {

    public static void main(String[] args) throws Exception {
        new HogeApplication().run(args);
    }

    @Override
    public String getName() {
        return "hoge-session-auth";
    }
    @Override
    public void initialize(Bootstrap<HogeConfiguration> bootstrap) {
        bootstrap.addBundle(new ViewBundle<HogeConfiguration>() {
            @Override
            public ImmutableMap<String, ImmutableMap<String, String>> getViewConfiguration(HogeConfiguration config) {
                return config.getViewRendererConfiguration();
            }
        });
{
    }

    @Override
    public void run(HogeConfiguration configuration,Environment environment) throws Exception {
        environment.jersey().register(AuthFactory.binder(new SessionUserAuthFactory<HogeUser>(new HogeSessionAuthenticator(), "HOGE USER", HogeUser.class)));

        final LoginResource loginResource = new LoginResource();
        environment.jersey().register(loginResource);
        final HogeUserResource hogeUserResource = new HogeUserResource();
        environment.jersey().register(hogeUserResource);
    }
}

jp.hoge.app.HogeConfiguration.java
package jp.hoge.app;

import javax.validation.constraints.NotNull;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;

import io.dropwizard.Configuration;

public class HogeConfiguration extends Configuration {

    @NotNull
    private ImmutableMap<String, ImmutableMap<String, String>> viewRendererConfiguration =  ImmutableMap.of();

    @JsonProperty("viewRendererConfiguration")
    public ImmutableMap<String, ImmutableMap<String, String>> getViewRendererConfiguration() {
        return viewRendererConfiguration;
    }
}

resources/jp/hoge/app/views/login.ftl
<#-- @ftlvariable name="" type="jp.hoge.app.views.LoginView" -->
<html>
  <head><meta charset="UTF-8"></head>
  <body>
   <h1>ログイン</h1>

   <form action="login/signup" method="post">
     <p>Eメール : <input type="text" name="email" size="40"></p>
     <p>パスワード : <input type="password" name="password" size="40"></p>
     <input type="submit" value="ログイン">
   </form> 
  </body>
</html>
resources/jp/hoge/app/views/mypage_top.ftl
<#-- @ftlvariable name="" type="jp.hoge.app.views.LoginView" -->
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
   <!-- calls getUser().getUserId() and sanitizes it -->
   <h1>Hello, ${user.userId?html}!</h1>
  </body>
</html>
hoge.yml
# the key needs to match the suffix of the renderer
viewRendererConfiguration:
    .ftl:
        strict_syntax: yes
        whitespace_stripping: yes
server:
  applicationConnectors:
    - type: http
      port: 8082
    - type: https
      port: 8443
      validateCerts: false
  adminConnectors:
    - type: http
      port: 8084
    - type: https
      port: 8444
      validateCerts: false
build.gradle

plugins {
  id 'java'
  id 'com.github.johnrengelman.shadow' version '1.2.0'
}

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'application'

defaultTasks 'shadowJar'

project.ext {
    authorName = 'Hogeo HOGETA'
    dropwizardVersion = '0.8.0'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8
group = 'jp.hoge.app'
version = '0.1'

applicationName = 'hoge-session-auth'
mainClassName = 'jp.hoge.app.HogeApplication'

repositories {
    mavenLocal()
    mavenCentral()
}

def defaultEncoding = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = defaultEncoding
tasks.withType(GroovyCompile)*.groovyOptions*.encoding = defaultEncoding

dependencies {
    // 全部は要らないけど消すのめんどいんで
    compile "io.dropwizard:dropwizard-core:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-assets:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-views:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-views-freemarker:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-migrations:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-hibernate:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-jdbi:${dropwizardVersion}"
    compile "io.dropwizard:dropwizard-auth:${dropwizardVersion}"

    testCompile "junit:junit:4.11"
    compile "io.dropwizard:dropwizard-testing:${dropwizardVersion}"

}

task wrapper(type: Wrapper) {
    gradleVersion = '2.2'
}

shadowJar {
    archiveName = String.format("%s-%s.jar", applicationName, version)
    mergeServiceFiles()
    exclude 'META-INF/*.SF'
    exclude 'META-INF/*.DSA'
    exclude 'META-INF/*.RSA'
}

jar {
    manifest {
        attributes(
            'Implementation-Title': applicationName,
            'Implementation-Version': version,
            'Built-By': authorName,
            'Built-Time': new Date(),
            'Main-Class': mainClassName,
            'Class-Path': configurations.compile.collect { it.getName() }.join(' ')
            )
    }
}