Posted at

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

More than 3 years have 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(' ')
)
}
}