2012年5月18日金曜日

Spring Social - Connection の管理

前回の『Facebook に接続』で、Spring Social を使い Facebook との接続(Connection)を確立しました。今回は、確立した Connection をローカルユーザーアカウントと紐付けて DB で管理する部分を作ります。

ConnectionRepository と UsersConnectionRepository
前回のような直球型では、アプリケーションを起動する度に facebook との“OAuth2 ダンス”が必要になります。一度確立した Connection オブジェクトを永続化し、再利用する仕組みを用意すれば、そうした手間がなくなります。

Spring Social リファレンスの“2.3 Persisting connections”を読むと Connection 管理の中核は、永続化された各(ローカル)ユーザーの Connection オブジェクトに対するデータアクセスインターフェースを提供する ConnectionRepository と、Connection のグローバルストアーとして機能する UsersConnectionRepository であることがわかります。

DB(MySQL)側の準備
今回は、UsersConnectionRepository の実装クラスである JdbcUsersConnectionRepository を使用します。そのためには DB の準備が必要です。“2.3.1 JDBC-based persistence”に倣い、以下のスキーマを考えました。
create table UserConnection (
  userId varchar(127) not null,
  providerId varchar(64) not null,
  providerUserId varchar(64),
  rank int not null,
  displayName varchar(127),
  profileUrl varchar(512),
  imageUrl varchar(512),
  accessToken varchar(127) not null,
  secret varchar(255),
  refreshToken varchar(127),
  expireTime bigint,
  constraint UserConnection_pkc primary key(userId, providerId, providerUserId),
  constraint UserConnection_idx1 unique index(userId, providerId, rank)
  );

@Configuration クラス
4. Connecting to Service Providers”に、Spring Social に標準添付されている ConnectController を利用するための設定手順が記されています。この手順を若干アレンジして、前回作ったコードでも利用できるようにしてみます。とは言ってもほぼ書き写しですが...

SocialConfig.java
まずは外枠の部分です。
package wrider.fb.test.config;

import javax.sql.DataSource;

import org.slf4j.Logger;
    :
import org.springframework.social.facebook.connect.FacebookOAuth2Template;

@Configuration
@ComponentScan(basePackages="wrider")
@ImportResource("/WEB-INF/applicationContext.xml")
@PropertySource("classpath:wrider/fb/test/config/facebook.properties")
public class SocialConfig {
  final static Logger logger = LoggerFactory.getLogger(SocialConfig.class);
    :
}
@Configuration でこれが Spring コンテナーを構成するクラスであることを指示しています。@ComponentScan@ImportResource は、それぞれ <component-scan/>, <import/>と同様に機能するアノテーションです。@PropertySource で、Environment インターフェースに食わせる .properties ファイル(facebook.properties)の所在を指示しています。

続いて諸々の bean の定義です。
@Autowired
  private Environment environment;
  
  @Autowired
  DataSource pooledDataSource;
  
  @Bean
  public TextEncryptor textEncryptor() {
    return Encryptors.noOpText();
  }
  
  @Bean
  public FacebookOAuth2Template facebookOAuth2Template() {
    return new FacebookOAuth2Template(
        environment.getProperty("facebook.clientId"), environment.getProperty("facebook.clientSecret"));
  }
  
  @Bean
  public FacebookConnectionFactory facebookConnectionFactory() {
    return new FacebookConnectionFactory(
        environment.getProperty("facebook.clientId"), environment.getProperty("facebook.clientSecret"));
  }
  

DataSource には『Hibernate でデータアクセス(3)』で定義した“pooledDataSource”を DI しています。本体コード(FacebookTestController.java)での記述を簡素化するために FacebookOAuth2Template および FacebookConnectionFactory のビーンを定義しています。この中に

environment.getProperty("facebook.clientId"), environment.getProperty("facebook.clientSecret"));

という記述がありますが、ここで @PropertySource で指示した場所にあるファイルから値を読み込み設定しています。そのファイル(facebook.properties)の内容は以下のような感じです。

facebook.clientId=123456...
facebook.clientSecret=11aa22bb33cc..

続く ConnectionFactoryLocatorUsersConnectionRepository のビーン定義は、ほぼリファレンスの通りです。
@Bean
  @Scope(value="singleton", proxyMode=ScopedProxyMode.INTERFACES)
  public ConnectionFactoryLocator connectionFactoryLocator() {
    ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
    registry.addConnectionFactory(facebookConnectionFactory());
    return registry;
  }
  
  @Bean
  @Scope(value="singleton", proxyMode=ScopedProxyMode.INTERFACES)
  public UsersConnectionRepository usersConnectionRepository() {
    logger.info("**UsersConnectionRepository");
    return new JdbcUsersConnectionRepository(this.pooledDataSource, connectionFactoryLocator(), textEncryptor());
  }

次の ConnectionRepository と Facebook の scope はユーザーからのリクエストに応じて個別に生成されなければならないので "request" になります。

facebook ビーンでは、connectionRepository の findPrimaryConnection()メソッドで Connection の有無を調べ、ローカルユーザーに紐づいた Connection が DB に存在すれば facebook の API を返します。存在しなかった場合、つまり、当該ローカルユーザーが始めて facebook に接続を試みる場合は 未認証の FacebookTemplate を生成し返します。
@Bean
  @Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
  public ConnectionRepository connectionRepository() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null) {
      throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
    }
    logger.info("**ConnectionRepository: {}", authentication.getName());
    return usersConnectionRepository().createConnectionRepository(authentication.getName());
  }
  
  @Bean
  @Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
  public Facebook facebook() {
    Connection facebook = connectionRepository().findPrimaryConnection(Facebook.class);
    if (facebook != null) {
      if (!facebook.test()) {
        facebook.refresh();
        logger.info("**facebook connection has been refleshed");
      } else {
        logger.info("**facebook connection alived");
      }
      return facebook.getApi();
    }
    else {
      logger.info("**return FacebookTemplate");
      return new FacebookTemplate();
    }
  }

SocialExceptionResolver.java(抜粋)
FacebookTemplate を返されたユーザーが OAuth2 認証を必要とする処理(例えば、Profile の取得など)を呼び出した場合、NotAuthorizedException が発生します。これの処理方法として例えば、『例外処理』の要領で HandlerExceptionResolver の実装クラスで捕まえ、『Facebook に接続』で作成した FacebookTestController クラスの singnin()メソッドに飛ばす、といった方法が考えられます。
public class SocialExceptionResolver implements HandlerExceptionResolver,
    Ordered {
  final static Logger logger = LoggerFactory.getLogger(SocialExceptionResolver.class);

  private int order = Integer.MAX_VALUE;
  
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    ModelAndView mav = new ModelAndView();
    if (ex instanceof NotAuthorizedException) {
      logger.info("<<NotAuthorizedException>>");
      mav.setViewName("redirect:/facebooktest/signin.html");
      return mav;
    }
    else {
      return null;
    }
  }
  
  public int getOrder() {
    return this.order;
  }
}

ここで request scope のビーンに関する注意点が一つ。もし [servlet-name]-servlet.xml に以下の設定がある場合、

<aop:aspectj-autoproxy proxy-target-class="true"/>

ブート時に怒られるので一先ず proxy-target-class="false" にしておきます。

コントローラーの変更
以上を踏まえて前回のコントローラーを次のように変更しました。

FacebookTestController.java(抜粋)
signin()メソッドはこんな感じです。DI した facebookOAuth2Template の buildAuthorizeUrl()メソッドで authUrl を生成して facebook にリダイレクトしています。
@RequestMapping(value = "facebooktest/signin.html", method = RequestMethod.GET)
  public void signin(HttpServletResponse response) 
    throws IOException {
    
    OAuth2Parameters params = new OAuth2Parameters();
    params.setRedirectUri(callbackUrl);
    params.setScope("read_stream");
    String authUrl = facebookOAuth2Template.buildAuthorizeUrl(GrantType.AUTHORIZATION_CODE, params);
    logger.info("auth url: {}", authUrl);
    response.sendRedirect(authUrl);
    
  }

コールバックを受け取る hello()メソッドです。DI した facebookOAuth2Template の exchangeForAccess()メソッドで認証コードを交換し、生成した Connection が connectionRepository に登録されていなかったら addConnection()メソッドで追加し、登録されていたら updateConnection()メソッドで内容を更新しています。
@RequestMapping(value = "facebooktest/hello.html", method = RequestMethod.GET)
  public ModelAndView hello(@RequestParam(value = "code", required = false) String code) {
    
    ModelAndView mav = new ModelAndView();
    
    if (code == null) {
      mav.setViewName("redirect:signin.html");
      return mav;
    }
    
    AccessGrant accessGrant = facebookOAuth2Template.exchangeForAccess(code, callbackUrl, null);
    Connection<Facebook> connection = facebookConnectionFactory.createConnection(accessGrant);
    
    if (this.connectionRepository.findPrimaryConnection(Facebook.class) == null) {
      this.connectionRepository.addConnection(connection);
      logger.info(">>connection added");
    }
    else {
      this.connectionRepository.updateConnection(connection);
      logger.info(">>connection updated");
    }
    logger.info(">>access token: {}", accessGrant.getAccessToken());
    mav.setViewName("redirect:home/index.html");
    
    return mav;
  }

レファレンスを参考に facebook アカウントとアプリケーションのローカルアカウントを連携させる環境は構築できました。果たしてこのやり方が正しいのか否かは定かではありませんが....

これをベースに簡単なアプリケーションを作ってみましたが、(極)個人的には使えるものになりました。ソーシャルアプリの開発にはまりそうです。

2012年5月16日水曜日

Facebook に接続

今回は Spring Social です。Spring Social のリファレンス“2. Service Provider 'Connect' Framework”には以下のように記されています。
spring-social-core モジュールは、Facebook や Twitter のような SaaS プロバイダーへの接続を管理するサービスプロバイダー‘コネクションフレームワーク’を含む。このフレームワークは、アプリケーションがローカルユーザーアカウントとユーザーが外部のサービスプロバイダーに持っているアカウント間のコネクションを確立することを可能とする
前回までに、アカウント管理の基本的な仕掛けはできたので、これをベースに Facebook に接続したいと思います。

準備
Spring Social から以下のパッケージをダウンロードし、解凍後、/WEB-INF/lib とビルドパスに登録します。

spring-social-1.0.2.RELEASE.zip
spring-social-facebook-1.0.1.RELEASE.zip

また、リファレンス“1.4 Dependencies”を参考に足りないコンポーネントを追加します。私の場合、以下のものを追加しました。

Jackson JSON Processor
jackson-all-1.9.7.jar

App ID の取得
Facebook for Websitesに行き、“Authentication”に記された要領に従いウェブサイトの App ID(と App Secret)を取得します。その際、アプリの「表示名」や「ドメイン」の他、OAuth2 認証時のリダイレクト(コールバック)先となる「サイト URL」を予め決めておく必要があります。

因みに私の場合、localhost で開発しているので図のような感じになります。

権限とポートのマッピング
前回、アプリケーションにアカウントを作成したユーザーに ROLE_USER という権限を割当てる仕掛けを作りこみました。今回の目的は、そうしたアプリ内のローカルユーザーと Facebook アカウントを(access token で)結びつけることです。言い換えれば、ROLE_USER という権限を付与された認証済みのローカルユーザーだけにアプリケーション(facebooktest)の使用を許可するということです。この要件を踏まえて、springSecurityConf.xml の <security:http/> 要素内に以下の行を追加しました。接続は https です。
  <security:http auto-config="true">
    :
    <!-- social service -->
    <security:intercept-url pattern="/facebooktest/**" requires-channel="https" access="ROLE_USER"/>
    :
  </security:http>



facebook に接続 - 直球型
リファレンス“2.2.1 OAuth2 service providers”に OAuth2 のフローが記されています。その下にあるのが Facebook に接続するコード例です。これに倣い、アプリケーションの認証からアクセストークンを受け取るまでのコードを作ってみます。

まずは、Facebook にアプリケーションの認証要求を送信する部分。フローは以下の通りです。
  1. FacebookConnectionFactory のコンストラクター引数に Facebook から発行された App Id と App Secret をセットしてインスタンス化し
  2. それから OAuth2 フローを制御するサービスインターフェース OAuth2Operationsを取得します。
  3. OAuth2Parameters オブジェクトに Facebook に登録したサイト URL(コールバックURL)をセットし
  4. OAuth2Operations の buildAuthorizeUrl() メソッドで Facebook にアプリケーションを認証してもらうための URL を生成します。
  5. 生成した URL にリダイレクトします。

以下が実際のコードです。facebooktest/signin.html を呼び出すと Facebook との“OAuth2 ダンス”が始まります。callbackUrl は、コールバック URL です。
@Controller
public class FacebookTestController {

  private OAuth2Operations oauthOperations;
    :
  @RequestMapping(value = "facebooktest/signin.html", method = RequestMethod.GET)
  public void signin(HttpServletResponse response) 
    throws IOException {
    FacebookConnectionFactory connectionFactory = new FacebookConnectionFactory("[App Id]", "[App Secret]");
    oauthOperations = connectionFactory.getOAuthOperations();
    OAuth2Parameters params = new OAuth2Parameters();
    params.setRedirectUri(callbackUrl);
    String authorizeUrl = oauthOperations.buildAuthorizeUrl(GrantType.AUTHORIZATION_CODE, params);
    logger.info("auth url: {}", authorizeUrl);
    response.sendRedirect(authorizeUrl);
  }
}

生成した authorizeUrl の内容はこんな感じです。

https://graph.facebook.com/oauth/authorize?client_id=[App Id]&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A8443%2Fishtar%2Ffacebooktest%2Fhello.html

Facebook にログインしていない状態で facebooktest/signin.html にアクセスすると下図の画面が現れます。


Facebook にログインするとコールバック URLにリダイレクトされてきます。それを受け止めるのが以下のコードです。
  @RequestMapping(value = "facebooktest/hello.html", method = RequestMethod.GET)
  public ModelAndView hello(@RequestParam(value = "code", required = true) String code) {
    
    ModelAndView mav = new ModelAndView();
    
    if (code == null) {
      mav.setViewName("redirect:signin.html");
      return mav;
    }
    
    AccessGrant accessGrant = oauthOperations.exchangeForAccess(code, callbackUrl, null);
    Connection<Facebook> connection = facebookConnectionFactory.createConnection(accessGrant);
    logger.info(">>access token: {}", accessGrant.getAccessToken());
    
    Facebook fb = connection.getApi();
    FacebookProfile fbProfile = fb.userOperations().getUserProfile();
    mav.addObject("fbProfile", fbProfile);
    mav.setViewName("home/index");
    
    return mav;
  }
  1. まず、リクエストパラメーターにFacebookからの認証コード“code”が含まれていなかったら前述の signin.html にリダイレクトします。
  2. OAuth2Operations の exchangeForAccess()メソッドで Facebook から受け取った認証コードを送り返し、AccessGrantを受け取ります。
  3. 受け取った AccessGrant に含まれている Access Token を使って Connection オブジェクトを生成します。
  4. Facebook インターフェースの userOperations()メソッドを介して Facebook に登録しているプロフィールを取得し、ModelAndView にセットして View を呼び出しています。

以上で、Facebook に接続するという目的は達成です。

2012年5月10日木曜日

DAO認証

今回は「Hibernate でデータアクセス(1)」以降で作成したアカウント管理の仕掛けに Spring Security の認証機能を取り入れます。

<authentication-manager/>への登録
まず「Anonymous 認証とポートマッピング」で触れた <authentication-manager/>に DaoAuthenticationProvider を登録します。
<!-- Authentication Manager-->
  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="anonymousAuthenticationProvider"/>
    <security:authentication-provider ref="daoAuthenticationProvider"/>
  </security:authentication-manager>

daoAuthenticationProvider の定義
DaoAuthenticationProvider は、AuthenticationProviderインターフェースの実装クラスの一つで、クライアントから来た認証要求と、UserDetailsService を介してロードした UserDetails を基づき認証処理を行います。

従って DaoAuthenticationProvider を使うためには、UserDetailsService の実装クラスを指定する必要があります。今回は、既にデータベースに作成してある UserProfile テーブルを活用したいと考えているので JdbcDaoImpluserDetailsService として登録します。
<!-- DAO Authentication -->
  <bean id="daoAuthenticationProvider"
    class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
    <property name="userDetailsService" ref="userDetailsService"/>
  </bean>

userDetailsService の定義
この JdbcDaoImpl の役割は、DB から JDBC 経由で認証に必要なデータを持ってくることで API ドキュメントにある通りデフォルトのスキーマが定義されています。もちろん、それに合わせて DB に新しいテーブルを作成するというのも一つの選択肢ではあります。しかし、JdbcDaoImpl やそのサブクラスである JdbcUserDetailsManager には DB へのクエリー文字列をカスタマイズできるメソッドが用意されています。今回は既存テーブルに対する若干の変更とクエリー文字列のカスタマイズという戦略を採ることにします。
<bean id="userDetailsService"
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
    <property name="dataSource" ref="pooledDataSource"/>
    <property name="usersByUsernameQuery"
      value="select email as username, password, enabled from UserProfile where email = ?"/>
    <property name="authoritiesByUsernameQuery"
      value="select email as username, authority from Authorities where email = ?"/>
  </bean>

既存のテーブル"UserProfile"にはemail, passworod, firstName, lastName, penName, ..といったカラムがあります。JdbcDaoImpl はデフォルトでは DEF_USERS_BY_USERNAME_QUERY で定義されているクエリーを発行するので、上図のように <property/> を使って既存テーブルに合わせます。

DB 側の調整
enabled については、既存テーブルになかったので alter table UserProfile .. で追加しました。一方、DEF_AUTHORITIES_BY_USERNAME_QUERY については、authorities というテーブルは無かったため、以下のような感じで作成しました。

create table Authorities(
  email varchar(255) not null,
  authority varchar(255) default 'ROLE_USER',
  constraint Authorities_fk1 foreign key(email)
    references UserProfile(email)
    on delete cascade
    on update cascade
  );

この authorities は、UserProfile テーブルに対して n:1 の関係になりますが、アカウント作成時にデフォルトのロール"ROLE_USER"が一つ、自動的にできているという状況を作るために次のようなトリガーを定義しました。

delimiter //
create trigger reflectNewUser after insert
  on UserProfile for each row begin
      insert into Authorities set email = new.email;
      insert into Cookies set pid = new.pid;
  end//
delimiter ;

別の方法として、例えば登録ユーザーの権限は無条件で ROLE_USER ただ一つで追加・変更を行わないというのであれば DEF_AUTHORITIES_BY_USERNAME_QUERY のクエリーを
select email, 'ROLE_USER' from UserProfile where email = ?
とすれば、新しいテーブルを必要がなくなります。

<security:http/>の変更
以上の作業で登録ユーザーには ROLE_USER という権限が割り当てられるようになったので、<security:http/>の内容も少し変えることにしました。/account のメニューには今のところ、ログイン画面、ログアウト後のさよなら画面、登録情報変更画面があります。また、/welcome 下には種々の登録者向けメニューを用意する予定です。これらを考慮して変更を加えたのが下図です。

今回から、http と https で別の cookie を発行するようにしましたが、<security:logout/>の設定で、ログアウト時にこれらを削除するよう指定できるのは便利だと思いました。ここで使った各種エレメントの詳細は“The Security Namespace”に記載されています。
<security:http auto-config="true">
    
    <!-- account service -->
    <security:intercept-url pattern="/account/**" requires-channel="https" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    <security:intercept-url pattern="/account/myprofile.html" access="ROLE_USER"/>
    
    <security:form-login login-page="/account/login.html"/>
    <security:logout logout-url="/account/logout.html" logout-success-url="/account/seeyou.html"
      delete-cookies="NSID, SSID" invalidate-session="true"/>
    
    <!-- welcome service -->
    <security:intercept-url pattern="/welcome/**" requires-channel="http" access="ROLE_USER"/>
    
    <!-- http/https port mappings -->
    <security:port-mappings>
      <security:port-mapping http="8080" https="8443"/>
    </security:port-mappings>
  </security:http>


DAO レイヤー
インターフェースを定義して、実装クラスを作成という基本に則って DAO レイヤー、サービスレイヤーを作ります。今回使いたいのは認証だけなので DAO レイヤーの実装クラス AuthManagerImpl は以下のように至ってシンプル、引数として受け取った emailpasswordUsernamePasswordAuthenticationToken インスタンスを生成して daoAuthenticationProvider の authenticate() メソッドに渡すだけです。これで認証が失敗したら AuthenticationException がスローされるので「例外処理」の要領で作った ExceptionResolver で特定のページ(例えば、ログイン画面)に飛ばすことができます。

AuthManagerImpl.java(抜粋)
package wrider.dao;
    :

@Repository
public class AuthManagerImpl implements AuthManager {
 
 @Autowired
 private DaoAuthenticationProvider daoAuthenticationProvider;
 
 public Authentication authenticate(String email, String password) {
  return this.daoAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(email, password));
 }

}

サービスレイヤー
サービスレイヤーでは、コントローラーから受け取った IdCard オブジェクトから emailpassword を取り出して、上記 authManager を呼び出しているだけです。認証が成功した場合は、DAO から返された Authetication オブジェクトをそのまま、呼び出し元に返すだけです。

AuthServiceImpl.java(抜粋)
package wrider.service;
    :
    
@Service
public class AuthServiceImpl implements AuthService {
  
  @Autowired
  private AuthManager authManager;
  
  public Authentication authenticate(IdCard idCard) {
    return authManager.authenticate(idCard.getEmail(), idCard.getPassword());
  }

}

コントローラー
コントローラー AccountController では、ログインフォームに入力されたデータのバリデーションと認証結果(Authentication オブジェクトとして返ってくる)の SecurityContextHolder への保存を行っています。

AccountController.java(抜粋)
:
@Controller
@SessionAttributes({"idCard", "userProfile", "confirmationUserProfile"})
public class AccountController {
  
  @Autowired
  private AuthService authService;
    :
  @RequestMapping(value="/account/login.html", method=RequestMethod.POST)
  public ModelAndView login(
      @ModelAttribute("userProfile") UserProfile userProfile,
      @Valid IdCard idCard, BindingResult br,
      HttpServletRequest request, HttpServletResponse response, HttpSession hsession) {
    
    ModelAndView mav = new ModelAndView();
    
    if (br.hasErrors()) {
      mav.getModel().putAll(br.getModel());
      mav.setViewName("account/login");
    }
    else {
      SecurityContextHolder.getContext().setAuthentication(this.authService.authenticate(idCard));
      userProfile = accountService.getProfile(idCard);
      setCookies(response, request, hsession, userProfile);
      
      mav.addObject("userProfile", userProfile);
      mav.setViewName("redirect:../welcome/home.html");
      
    }
    return mav;
  }
    :
}

以上のコードを実行した結果得られた SecurityContext の一例が以下です。
..core.context.SecurityContextImpl@85a9d015:
  Authentication:
   ..authentication.UsernamePasswordAuthenticationToken@85a9d015:
    Principal: ..core.userdetails.User@23506b1: Username: xxx@yyyy.jp; 
    Password: [PROTECTED]; 
    Enabled: true; 
    AccountNonExpired: true; 
    credentialsNonExpired: true; 
    AccountNonLocked: true; 
    Granted Authorities: ROLE_USER; 
    Credentials: [PROTECTED]; 
    Authenticated: true; 
    Details: null; 
    Granted Authorities: ROLE_USER

作りこみ次第でもっと様々な情報を保存することもできます。既存システムとの統合も考慮された Spring Security は「全てを我々に合わせて作り変えろ」ではなく「必要な部分だけ使ってね」といった感じの距離感を持ったフレームワークだと思いました。

尚、Spring Security の認証メカニズムについては、リファレンスの“Technical Overview”や“7.1 The AuthenticationManager, ProviderManager and AuthenticationProviders”に詳しく記されています。

2012年5月7日月曜日

Anonymous 認証とポートマッピング

ある URL では http、別の URL では https で接続したいときがあります。今回は Spring Security を使って、その辺の機能を追加します。

準備 - Spring Security
Spring Security のサイトから spring-security-[version].RELEASE.zip をダウンロード後、解凍し、必要なファイルを /WEB-INF/lib にコピーし、ビルドパスにも追加します(現時点の最新バージョンは spring-security-3.1.0.RELEASE です)。

今回 cas, ldap, openid, remoting は明らかに使いませんし、samples も必要ないので、それら以外を上記の要領で環境に追加します。

名前空間
まず“Security Namespace Configuration”に従い、コンテキストファイルに Spring Security の設定を記述するための名前空間を追加します。私の場合 springSecurityConf.xml というファイルを作成し、/WEB-INF/conf に置くことにしました。

<?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:security="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/security 
         http://www.springframework.org/schema/security/spring-security-3.1.xsd">

Authentication Manager の構成
Spring Security の機能を使うためには Authentication Manager が構成されていなければなりません。今回は Anonymous 認証で行きたいと考えているので“Anonymous Authentication”の解説と名前空間リファレンス <security:authentication-manager/> を参考に Authentication Manager を構成します。

すると、こんな感じです。
  <!-- Authentication Manager-->
  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="anonymousAuthenticationProvider"/>
  </security:authentication-manager>
  
  <!-- Anonymous Authentication -->
  <bean id="anonymousAuthenticationProvider"
    class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
    <property name="key" value="psid"/>
  </bean>
  
  <bean id="anonymousAuthenticationFilter"
    class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
    <property name="key" value="psid"/>
    <property name="userAttribute" value="anonymousUser, ROLE_ANONYMOUS"/>
  </bean>

Anonymous Authentication
これでも動きます。ただ、AnonymousAuthenticationFilter に関しては、API ドキュメントを見ると setKey と setUserAttribute は Deprecated use constructor injection instead となっています。気になったので以下のように constructor injection を使う方法でも試してみたら <property/> で設定した時と「同じように」動きました。
  <bean id="anonymousAuthenticationFilter"
    class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
    <constructor-arg index="0" type="java.lang.String" value="psid"/>
  </bean>

リファレンスと GrepCode を見ると Spring Security の Anonymous 認証では、..Filter が ..Token の生成と SecurityContextHolder への追加、..Provider が ..Token の認証を行います(..の箇所は"AnonymousAuthentication")。つまり、SecurityContextHolder を見れば Anonymous 認証の内容が確かめられるということになります。で、確かめてみました。

 ..core.context.SecurityContextImpl@90572420:
  Authentication:
   ..authentication.AnonymousAuthenticationToken@90572420:
    Principal: anonymousUser; 
    Credentials: [PROTECTED]; 
    Authenticated: true; 
    Details:
     ..web.authentication.WebAuthenticationDetails@255f8:
      RemoteIpAddress: 127.0.0.1; 
      SessionId: A1B2C3...XYZ; 
      Granted Authorities: ROLE_ANONYMOUS


<property/>, <constructor-arg/> いずれの方法でも上と同じような内容になりました。

<security:http/> の設定
Authentication Manager が構成できたら、いよいよ本丸です。<security:http/> を参考に、URL パターンとポートをマッピングします。シナリオは

 ・/welcome は http
 ・/account は https

という単純なものにしました。
  <security:http pattern="/style/**" security="none"/>
  
  <security:http auto-config="true">
    
    <!-- account service -->
    <security:intercept-url pattern="/account/**" requires-channel="https" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    
    <!-- welcome service -->
    <security:intercept-url pattern="/welcome/**" requires-channel="http" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    
    <!-- http/https port mappings -->
    <security:port-mappings>
      <security:port-mapping http="8080" https="8443"/>
    </security:port-mappings>
    
  </security:http>

<security:intercept-url/>pattern で URL パターン、それに対応するチャネル(http/https)とアクセス属性をそれぞれ requires-channel, access で指定しています。

そして <security:port-mappings/> で、http, https のそれぞれについて使用するポート番号を指定します。

これで完了です。私の場合、残る作業は、作成した springSecurityConf.xml を以下のように web.xml に登録するだけです。
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
    /WEB-INF/applicationContext.xml
    /WEB-INF/conf/springSecurityConf.xml
    </param-value>
  </context-param>

今回は割りと単純な例でしたが、Spring Sucurity にはまだまだ沢山の機能があります。単純でも一度設定作業に慣れておけば、そうした機能を使いたい時の閾がある程度は低くなると思います。個人的には DB との連動や Spring Social を使ったソーシャルメディアへの接続に興味が沸いてきました。いずれ...というより、(おそらく)次回からは、その辺に挑戦してみたいと思います。