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

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

```java
/* TODO コードとか作りとか色々汚いので使うときにはちゃんと修正する */
```

* Cookieにsidが設定されていない場合はログイン画面に飛ばす
* ログイン時にCookieにsidを設定してあげる


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

```java: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;
	}

}

```

```java: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();
    }
}

```

```java: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;
	}
}

```


```java: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();
	}
	
}

```

```java: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;
	}
	
}

```

```java: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();
    }
}
```

```java: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);
    }
}

```

```java: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());
	}
	
}
```

```java: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;
	}

}

```

```java: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);
	}
}

```

```java: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;
    }
}

```

```html: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>
```

```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>
```

```yaml: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
```

```groovy: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(' ')
            )
    }
}

```