90-2.自作の認証手形発行所(AuthenticationProvider)を使用するサンプル(Security2.x系)

概要

Spring Security 2.x系のサンプルです。

自作したクラスを使いたいときにどうすればよいか?

そのサンプルと思っていください。

ただし、フィルタはやり方が少し違うので、フィルタ以外のカスタマイズサンプルと思っていただければと思います。

サンプル全体の説明もお読みください。

目標

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

最近はセキュリティ要請が厳しくなり、企業によってはシステムのログインに、「指紋認証」を使用するところが増えました。

指紋認証は、指紋認証サーバを立て、認証サーバと通信をして認証OK/NGの結果を得るものがほとんどです。

これをSpring Securityでどのように実現したらよいでしょうか?

【考え方】

まず、どの機能をカスタマイズすればよいかを考えます。

そのためには、内部の仕組みの記事の図の左側(処理フロー)を見ます。

このうちどれをカスタマイズすればよいかを考えますが、指紋認証は認証行為なので、認証処理フィルタに行き当たります。

さらに、実際に認証をして手形を発行しているのは、認証処理フィルタ内の認証手形発行所であることが分かります。

ですので、AuthenticationProviderを自作すればよいと推測できます。

【指紋認証独自のもう一つの問題】

指紋認証のログインを実現するのにはもう1つ問題があります。

指紋認証に必要な情報は、ID、指紋をスキャンした情報、の他に、チャレンジと呼ばれるワンタイムトークンが必要になることが多いです。

不正使用を防ぐためなのですが、Spring Securityのデフォルトでは2つの情報(ID、PW)しか送れません。

こんなときはどう設定したらよいでしょうか?

このサンプルは、3つ以上の情報をSpring Securityに送りたいときのサンプルでもあります。

※このあたりのことは実はForumなどで議論されているの参考までにリンクを記述します。

Spring Security FAQ

【実装】

ここではログイン画面から3つの情報をsubmitし、自作のProviderで受け取って指紋認証処理をすることにします。

ただし、指紋認証処理自体は使用する指紋認証サーバによっても違うので、ここでは扱いません。

いつでもOKを返すようにします。

また、指紋認証サーバではアクセス認可の機能を持つことはないため、アクセス認可はSpring Securityの機能を使用します。

実際のサンプル

自作の認証手形発行所(指紋認証)クラス(/src/com/my/security/FingerAuthenticationProvider.java)

package com.my.security;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.springframework.dao.DataAccessException;

import org.springframework.security.Authentication;

import org.springframework.security.AuthenticationException;

import org.springframework.security.AuthenticationServiceException;

import org.springframework.security.BadCredentialsException;

import org.springframework.security.providers.AuthenticationProvider;

import org.springframework.security.providers.UsernamePasswordAuthenticationToken;

import org.springframework.security.userdetails.UserDetails;

import org.springframework.security.userdetails.UserDetailsService;

import org.springframework.util.StringUtils;

public class FingerAuthenticationProvider implements AuthenticationProvider {

static Log log = LogFactory.getLog(FingerAuthenticationProvider.class);

static public final String PASSWORD_DELIMITER = ":";

private UserDetailsService userDetailsService;

@Override

public Authentication authenticate(Authentication auth)

throws AuthenticationException {

log.debug("独自authenticateを通っています");

//パスワードを展開してリクエストの各種値を取得する

String id = auth.getName();

String complexPasswd = auth.getCredentials().toString();

//チャレンジとパスワードに展開する

int pos = complexPasswd.indexOf(PASSWORD_DELIMITER);

if(!StringUtils.hasText(complexPasswd) || pos < 0){

throw new BadCredentialsException("認証エラー発生", id);

}

String challenge = complexPasswd.substring(0, pos);

String passwd = complexPasswd.substring(pos+1);

if(!StringUtils.hasText(challenge)){

log.debug("challengeが空です。");

throw new BadCredentialsException("認証エラー発生", id);

}

//認証

if(fingerAuthenticate(id, passwd, challenge)){

auth = createAuthentication(id, (UsernamePasswordAuthenticationToken) auth);

}else{

throw new BadCredentialsException("認証エラー発生", id);

}

return auth;

}

protected boolean fingerAuthenticate(String id, String passwd, String challenge){

//実際には指紋認証の処理を記述する

return true;

}

@Override

public boolean supports(Class token) {

return UsernamePasswordAuthenticationToken.class.isAssignableFrom(token);

}

/*

* 認証手形を発行する(作り直す)

* たいていの場合、指紋認証はロールの情報を持っていない。

* DBにIDとロールを登録しているので、DBなどから情報を取得する処理をする。

*/

protected final Authentication createAuthentication(String username, UsernamePasswordAuthenticationToken token) throws AuthenticationException

{

UsernamePasswordAuthenticationToken auth;

UserDetails loadedUser;

try{

loadedUser = getUserDetailsService().loadUserByUsername(username);

}catch (DataAccessException repositoryProblem) {

throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);

}

if (loadedUser == null) {

throw new AuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");

}

//指紋認証のため特にパスワードは不要。

auth = new UsernamePasswordAuthenticationToken(token.getPrincipal(), "dummy passwd", token.getAuthorities());

auth.setDetails(loadedUser);

return auth;

}

public UserDetailsService getUserDetailsService() {

return userDetailsService;

}

public void setUserDetailsService(UserDetailsService userDetailsService) {

this.userDetailsService = userDetailsService;

}

}

【クラスについて】

単純に、AuthenticationProviderインターフェースをimplementsして作成するだけです。

少しややこしいのは、ログイン情報(ID/PW)オブジェクトと、認証OK後に発行される認証手形オブジェクトが同じクラスだということです。

つまり、引数として渡されるAuthenticationは、ログイン情報としてのオブジェクトでID/PWしか保持されていませんが、

return値として返されるAuthenticationは認証OKを表す認証手形で、ロールなども設定されたものを返します。

【supportsメソッドについて】

tokenがマッチした場合だけ、このクラスの認証処理が呼ばれます。

ですので自作の Authenticationを作成した場合、supportsがtrueを返す認証手形発行所(AuthenticationProvider)がないと、

何も認証処理がされないので、自作の場合は注意です。

【3つ以上の情報の引き渡しについて】

ログインJSPファイルも見てもらった方が良いですが、実は2つの情報しか送っていません。

パスワードに、コロン(:)区切りで、"チャレンジトークン:指紋情報"という形で設定してsubmitしています。

上記のクラスでは、それを分解して処理しているだけです。

Spring設定ファイル(/WEB-INF/spring/applicationContext-security-finger.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-3.0.xsd

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

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

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

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

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

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

<!-- 指紋認証 -->

<!-- SpringSecurityのメイン設定

-->

<sec:http access-denied-page="/403.jsp" path-type="ant" auto-config="false">

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

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

<sec:intercept-url pattern="/403.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

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

<sec:form-login login-page="/login.jsp" default-target-url="/top.jsp"

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

<sec:logout logout-url="/logout" logout-success-url="/login.jsp" invalidate-session="true"/>

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

</sec:http>

<!-- 指紋認証の認証手形発行所 -->

<bean id="fingerAuthenticationProvider" class="com.my.security.FingerAuthenticationProvider">

<sec:custom-authentication-provider/>

<property name="userDetailsService" ref="userService"/>

</bean>

<!--

ログインユーザの設定。AuthenticationのUserに設定される。

自作Providerが独自の認証を行うためpasswordは実際には使用されない。

-->

<sec:user-service id="userService">

<sec:user name="taro" password="taro" authorities="ROLE_ADMIN"/>

<sec:user name="hanako" password="hanako" authorities="ROLE_READ"/>

</sec:user-service>

</beans>

【自作したProviderの設定の仕方】

実はSpring Securityではカスタマイズ用のタグが用意されています。

それを設定するだけです。

上記で、beanのfingerAuthenticationProviderの中でcustom-authentication-providerタグが使われているのが分かるでしょうか?

このようにbeanタグ内にこのタグを設定するだけです。

※custom-xxx系のタグはおおよそこのような使い方になります。

(3.x系ではやり方が違いますのでご注意ください)

ログイン画面サンプル(/login.jsp)

<%@ page language="java" pageEncoding="utf-8"%>

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

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

<%

out.print(session.getId());

%>

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

<html>

<head>

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

<script type="text/javascript">

//<!--

function submitTest(){

var challenge = document.getElementById("challenge").value;

var token = document.getElementById("token").value;

var complexPasswd = challenge + ":" + token;

alert(complexPasswd);

document.getElementById("passwd").value = complexPasswd;

document.getElementById("f").submit();

return true;

}

//-->

</script>

<title>ログイン</title>

</head>

<body>

<div class="explanation">

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

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

</font>

</c:if> ログインIDを入力してください。<br>

本当はhiddenにするべきものも、動きが分かるように表示してます。</div>

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

>

challenge:<input type="text" id="challenge" name="challenge" value="faweorq23p4rjtq3" disabled><br>

token:<input type="text" id="token" name="token" value="AB3B0A00000000000" disabled><br>

<table class="login">

<tr>

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

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

</tr>

<tr>

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

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

</tr>

</table>

<input type="button" name="login" value="ログイン" onclick="javascript: submitTest();">

</form>

</body>

</html>

ログインボタンを押したときに、javascriptで、パスワードのinputタグの中に「チャレンジトークン:認証情報」を入れています。

これで、3つ以上の値をSpring Securityに送る場合も対応できます。

Created Date: 2013/07/07