90-4.ログイン情報をID/PW以外に追加するサンプル
(Security5.x系)

概要

ここでは、SpringSecurity5系のカスタマイズサンプルを見ていきます。

SpringSecurityは、通常はID/PWしか扱いませんが、指紋認証や、マルチテナントなどを扱う場合、追加の情報を扱いたいことがあります。

そのような場合のサンプルです。

他にも方法はありますので一例です。

【補足】

このブログ全体に言えるのですが、Springの設定についてはXMLの形式で記述します。

というのは、最近はJavaConfigの記事が多くなりXMLの設定の記事が少なくなったことと、

XMLの記述から類推してJavaConfigは書けると思われるからです。

目標

まずはゴールを示します。

マルチテナントのWEBを作ることをイメージします。

この場合、ログイン情報としてID/PW以外にテナントIDなどがほしくなるかもしれません。

しかし、デフォルトのSpringSecurityでは、ID/PWしか認証処理時に受け取れません。

これを改善するため、以下のカスタマイズ機能を追加してみます。

①ログイン認証をID、PW、テナントIDで行う

②AuthenticationにテナントIDを追加し、JSPなどでテナントIDを出力できるようにする。

上記の②のAuthenticationは認証手形ですが、通常はログインIDとPWとロールを保持して、JSPなどで出力できます。

出力できる情報を増やす機能追加をしてみます。

実際のサンプル

自作の認証手形Authentication(test/TenantAuthenticationToken.java)

public class TenantAuthenticationToken extends UsernamePasswordAuthenticationToken {

private static final long serialVersionUID = 1L;

private int tenantId = -1;

//未認証の認証手形を作成するコントラクタ(ログインの入力ID/PWを保存するときに使用します)

public TenantAuthenticationToken(Object principal, Object credentials, int tenatId) {

super(principal, credentials);

this.tenantId = tenatId;

}

//認証済の認証手形を作成するコンストラクタ(認証OKのときにログイン済み情報を内部に持ちます)

public TenantAuthenticationToken(Member mem, Collection<? extends GrantedAuthority> authorities) {

super(mem.getLoginId(), mem.getLoginPw(), authorities);

this.tenantId = mem.getTenantId();

}

public int getTenantId() {

return tenantId;

}

}

自作のAuthenticationは、UsernamePasswordAuthenticationTokenを派生させて作ります。

getterとしてテナントIDを作成しておきます。基本的には変更できないようにすべきなのでsetterは作りません。

自作の認証処理フィルタ(test/TenantUsernamePasswordAuthenticationFilter.java)

public class TenantUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

public static final String PARAM_NAME_TENANT_ID = "tenantId";



/**

* リクエストパラメタから入力のAthentication(認証情報)を作成し、

* 内部で持つ正しいAuthenticationと比較して、ログインさせてよいかを決める。

*/

@Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)

throws AuthenticationException {

if (!request.getMethod().equals("POST")) {

throw new AuthenticationServiceException(

"Authentication method not supported: " + request.getMethod());

}

String username = obtainUsername(request);

String password = obtainPassword(request);

int tenantId = obtainTenantId(request);

if (username == null) username = "";

if (password == null) password = "";

username = username.trim();

//入力のログイン情報(認証手形(未認証))を作成

TenantAuthenticationToken authRequest = new TenantAuthenticationToken(username, password, tenantId);


return this.getAuthenticationManager().authenticate(authRequest);

}

protected int obtainTenantId(HttpServletRequest request) throws AuthenticationException{

String str = request.getParameter(PARAM_NAME_TENANT_ID);

try{

int id = Integer.parseInt(str);

return id;

}catch(Exception e){

throw new BadCredentialsException("tenantIdの値がおかしい。val=" + str, e);

}


}


//-----------------------------------------

//以下、面倒なのでヘルパー関数的に各種の設定Setterを作成


public void setAuthenticationFailureUrl(String defaultFailureUrl){

SimpleUrlAuthenticationFailureHandler handler = (SimpleUrlAuthenticationFailureHandler)getFailureHandler();

handler.setDefaultFailureUrl(defaultFailureUrl);

}


public void setFailureUrlParameter(String targetUrlParameter) {

SimpleUrlAuthenticationFailureHandler handler = (SimpleUrlAuthenticationFailureHandler)getFailureHandler();

handler.setDefaultFailureUrl(targetUrlParameter);

}


public void setDefaultTargetUrl(String url){

SavedRequestAwareAuthenticationSuccessHandler handler =(SavedRequestAwareAuthenticationSuccessHandler)getSuccessHandler();

handler.setDefaultTargetUrl(url);

}


public void setAlwaysUseDefaultTargetUrl(boolean bl){

SavedRequestAwareAuthenticationSuccessHandler handler =(SavedRequestAwareAuthenticationSuccessHandler)getSuccessHandler();

handler.setAlwaysUseDefaultTargetUrl(bl);

}


public void setTargetUrlParameter(String targetUrlParameter) {

SavedRequestAwareAuthenticationSuccessHandler handler =(SavedRequestAwareAuthenticationSuccessHandler)getSuccessHandler();

handler.setTargetUrlParameter(targetUrlParameter);

}


}

メインは先頭の青字のメソッドだけです。

後半は、DIをしやすくするための工夫なのであまり気にしなくて大丈夫です。

【メインのメソッドの処理内容】

処理内容ですが、このクラスは認証処理をします。

リクエストパラメタから認証手形(未認証)を作成して、認証チェックをするAuthenticationManagerに認証手形が正しいかをチェックしてもらいます。

AuthenticationManagerは、認証手形(未認証)が正しい場合、認証手形(認証済)を返します。

正しくない場合は、例外を発生します。

AuthenticationManagerでは、以下の自作の認証手形発行所(Provider)が認証チェックと認証手形(認証済)を発行する処理を行っています。

自作の認証手形発行所(test/TenantAuthenticationProvider.java)

public class TenantAuthenticationProvider implements AuthenticationProvider{

//MemberDaoはここでは示しません。各自で作成してみてください。

@Autowired

private MemberDao memberDao;

//パスワードのエンコード方法を指定

@Autowired

@Qualifier("passwordEncoder")

PasswordEncoder passwordEncoder;

/**

* リクエストパラメタから作られた入力authenticationを渡すと、認証を行い、

* 認証成功した場合は、ロールなどが設定されたログイン後のauthenticationを返す。

*/

@Override

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

TenantAuthenticationToken inputAuth = (TenantAuthenticationToken)authentication;


//正しいログイン情報の検索(本来はUserDetailsServiceが行う処理)

int tenantId = inputAuth.getTenantId();

String loginId = inputAuth.getPrincipal().toString();

Member correctMember = this.memberDao.obtainMember(tenantId, loginId);

if(correctMember == null) throw new BadCredentialsException("ログイン情報がおかしい。");


//ログイン認証チェック

String credentials = inputAuth.getCredentials().toString();

if(!this.passwordEncoder.matches(credentials, correctMember.getLoginPw())){

throw new BadCredentialsException("パスワードが間違っている。ログイン情報=" + inputAuth);

}


//認証成功なので、まずロールを作る。

List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();

roles.add(new SimpleGrantedAuthority(correctMember.getRole()));


//認証OKなので、認証済みのAuthenticationを作成して返す

TenantAuthenticationToken successAuth = new TenantAuthenticationToken(correctMember, roles);


return successAuth;

}

@Override

public boolean supports(Class<?> authentication) {

return TenantAuthenticationToken.class.isAssignableFrom(authentication);

}

}

【このクラスの動作】

このクラスは、認証成功したときに、認証済みの認証手形を提供します。

(認証手形はAuthenticationTokenクラスを指しています。この記事ではAuthenticationと略して記述することもあります。)

ログイン画面の入力情報である認証手形(未認証)が正しいかを判定し、正しければ認証OKのサインをした認証手形(認証済)Authenticationを発行します。

WEB上に良くある例題では、UserDetailsServiceもカスタマイズして、このクラスにDIしているかと思いますが、

ここでは自作のMemberDaoを使ってもっとシンプルに記述しています(MemberDaoは各自作成ください)。

Providerは、通常はUserDetailsServiceを使うのですが、別に無理に使わなくても問題ありません。

ですので、他でも使用するDaoを使って記述する方がシンプルかと思います。

<MemberDaoの仕様>

MemberDaoは、obtainMember()メソッドで、Memberクラスを返却するように作ってください。

Memberクラスはプロパティ(Setter/Getter)として、loginId, loginPw, tenantId を用意して、Daoで設定するようにしてください。

【PasswordEncoderクラスについて】

Spring5以前から存在するクラスですが、Spring5からデフォルトで使用するようになったようです。

PasswordEncoderクラスの役割は、パスワードを分かりにくい文字列(エンコード)にすることです。

このクラスを使用する場合、DBに保存するパスワードもエンコードしておく必要がありますが、それは世の中の流れとして当たり前になっているかと思います。

複数の派生クラスが存在しますのでどのようなエンコード方法があるかはJavadocを確認してみてください。

SpringSecurity設定ファイル(test/spring-security.xml)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:util="http://www.springframework.org/schema/util"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:sec="http://www.springframework.org/schema/security"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/util

http://www.springframework.org/schema/util/spring-util.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop.xsd

http://www.springframework.org/schema/security

http://www.springframework.org/schema/security/spring-security.xsd">

<!-- JSPタグでSpringSecurity式を使用したい場合に以下を定義する -->

<bean id="webexpressionHandler"

class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler"/>

<!-- SpringSecurity5のメイン設定

-->

<sec:http request-matcher="ant" auto-config="false" use-expressions="false"

entry-point-ref="loginUrlAuthenticationEntryPoint" >

<sec:access-denied-handler error-page="/error/403.html"/>


<!-- 認証なしで表示する画面の設定(これらを設定しないとどこにもアクセスできないWEBになります) -->

<sec:intercept-url pattern="/error/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

<sec:intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

<sec:intercept-url pattern="/include/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

<!-- 認証後(ログイン後)に表示する画面のロール設定 -->

<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>


<!-- ログイン設定(コメントアウトする) -->

<!--sec:form-login login-processing-url="/j_spring_security_check"

login-page="/login.html" default-target-url="/top.html"

always-use-default-target="false"

authentication-failure-url="/login.html?error=true" /-->

<!-- ログアウト設定 -->

<sec:logout logout-url="/logout" invalidate-session="true"/>

<sec:anonymous granted-authority="ROLE_ANONYMOUS"/>


<!-- HTTPヘッダで可能なセキュリティ対策

ここで設定できる内容は基本的にはApacheの設定ファイルの記述でも解決可能なことです。

アプリ側で対策するか、Apache側で対策するかは決めの問題です。

-->

<sec:headers>

<!-- クリックジャッキング対策 -->

<sec:frame-options policy="SAMEORIGIN" />

</sec:headers>


<!-- フィルタの挿入位置を指定する(既存フィルタと入れ替え)。 -->

<sec:custom-filter ref="tenantUsernamePasswordAuthenticationFilter"

position="FORM_LOGIN_FILTER"/>

</sec:http>

<!-- SpringSecurity5からPasswordEncoderが必要になったらしい。

本当は、org.springframework.security.crypto.bcrypt.BCryptPasswordEncoderクラス使用すべき。

使用する場合は、DBに保存するPWも暗号化しておくこと。

-->

<bean id="passwordEncoder"

class="org.springframework.security.crypto.password.NoOpPasswordEncoder" />

<!-- エントリポイント。自作Filter使用の時は記述必要。sec:httpタグの entry-point-ref属性に設定するbean -->

<bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">

<constructor-arg value="/login.html" />

</bean>

<!-- 自作フィルタ。自作の入力用の認証情報を作成するためのフィルタ。 -->

<bean id="tenantUsernamePasswordAuthenticationFilter" class="test.TenantUsernamePasswordAuthenticationFilter" >

<property name="authenticationManager" ref="authenticationManager" />

<property name="authenticationFailureUrl" value="/login.html?error=true" />

<property name="defaultTargetUrl" value="/top.html" />

<property name="alwaysUseDefaultTargetUrl" value="false" />

<property name="filterProcessesUrl" value="/j_spring_security_check" />

</bean>

<!-- 認証処理 -->

<sec:authentication-manager alias="authenticationManager">

<!-- 認証手形発行所 -->

<sec:authentication-provider ref="tenantAuthenticationProvider" />

</sec:authentication-manager>


<!-- 自作のログイン認証&Credencial作成処理(認証手形発行所) -->

<bean id="tenantAuthenticationProvider" class="test.TenantAuthenticationProvider" />

</beans>

通常のXML記述からの変更点は、色のついた部分だけですので、そこだけご覧いただければと思います。

sec:form-loginタグは削除します。

なぜかというと、このタグはTenantUsernamePasswordAuthenticationFilterクラスと同等のため、そのままDIすると

二重に同じ役割をするフィルタクラスを登録することになるため、エラーになるからです。

自作のフィルタクラスは、sec:custom-filterタグで元のフィルタと入れ替えをします。

あとは、自作したクラスをDIしていくだけです。

【sec:custom-filterタグによるフィルタ入れ替えについての補足】

sec:custom-filterタグにはpositionという属性がありますが、他にもbeforeとafterがあります。

・position ・・・指定のフィルタと自作のフィルタを入れ替える。

・before ・・・指定のフィルタの前に自作フィルタを挿入する。

・after ・・・指定のフィルタの後に自作フィルタを挿入する。

まずは、SpringSecurityのフィルタの順番をこの記事でご確認ください。

例えば、after="FORM_LOGIN_FILTER"の場合は、AuthenticationPerocessingFilterの前に自作のフィルタを挿入します。

AuthenticationPerocessingFilterはそのまま使用されます。

position="FORM_LOGIN_FILTER"の場合は、AuthenticationPerocessingFilterを自作フィルタで入れ替えるため、元のAuthenticationPerocessingFilterは実行されません。

sec:custom-filterタグで指定できるフィルタの位置の一覧について

FORM_LOGIN_FILTERのような位置を示すものが他にどんなものがあるかは、eclipseの場合ですが、

SpringSecurityのXMLのpositionの値を設定する箇所で、Ctrl+Spaceキーを押すとリストで出てきます。

JavaConfigとXMLのフィルタ名とのマッピングや説明はSpringSecurityのリファレンスに載っています。

Table 6.1. Standard Filter Aliases and Ordering

【passwordEncoderについて】

DIしているpasswordEncoderは、上記TenantAuthenticationProviderの説明でも書きましたが、パスワードをエンコードするクラスです。

NoOpPasswordEncoderクラスは何もエンコードしないテスト用クラスで、すでにDepricatedしていまして、将来、完全にクラスが削除される可能性が高いです。

このテスト用クラスを使用してしまったのは、DBに保存するパスワードをエンコードするのが面倒だったからです。

ですので、実戦で使用するときは、BCryptPasswordEncoderなどの正しいエンコードクラスを使用してください。

このクラスは、DBに保存されたエンコードされたPWと、ログイン画面で入力された未エンコードのパスワードを比較して、正しいかをチェックします。

チェックするときは、ログイン入力されたパスワードをエンコードし、エンコードされた文字列どうしを比較します。

ログイン画面(login.jsp)

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

<%@ page session="false" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>ログイン</title>

</head>

<body>

<h1>ログイン画面</h1>

<div class="explanation">

<c:if test="${param.error eq 'true'}">

<font color="red"> 入力されたユーザID、パスワードは不正です。<br>

</font>

</c:if> <br>

テスト用の画面。

</div>

<form id="f" action="<c:url value='j_spring_security_check'/>" method="post">

<table class="login">

<tr>

<th>ログインID</th>

<td><input type="text" name="username" value="<c:out value="${SPRING_SECURITY_LAST_USERNAME}"/>"></td>

</tr>

<tr>

<th>ログインPW</th>

<td><input type="text" id="passwd" name="password" ></td>

</tr>

</table>

<input type="text" id="tenantId" name="tenantId" value="1">

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

<input type="submit" name="login" value="ログイン" >

</form>

</body>

</html>

ログイン画面には、tenantIdのパラメタをPOSTする追加だけです。

ログイン成功後の画面(top.jsp)

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<!doctype html>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>トップ</title>

</head>

<body>

トップページです<br>

テナントIDは、<sec:authentication property="tenantId"/>です。

</body>

</html>

追加したtenantIdをJSPで出力するサンプルです。

赤字が出力部分です。簡単ですね!

Created Date: 2018/08/03