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