Spring Securityのクラス構造と拡張

ここまで基本的な設定の説明をしてきたが、ユーザ認証・アクセス制御は比較的AP固有の拡張が必要になりやすい分野である。

いざユーザ認証・アクセス制御の拡張が必要になった時に、どこを拡張すればよいかは、Securityタグの設定を理解しているだけでは対応が難しい。

そこで、Spring Securityの拡張が必要な場合に備えて、おおよそのクラス構造を説明しておきたい。クラス図では中心的な役割を果たすクラスのみ挙げている。

共通

まずはユーザ認証とアクセス制御で共通に利用される主体に関するクラス構造である。

図2: クラス図(共通) ※ 文字を認識できるjpg画像がサンプルに含まれているのでそちらをご覧になってほしい

Authentication はSpring Security形式(Spring Securityの各クラスが利用できる形式)の主体を表している。UsernamePasswordAuthenticationTokenはusernameとpasswordで本人性を確認するAuthenticationの実装クラスだ。

Spring Securityでは使わないが、画面に表示したり、機能から参照したりする際に必要になる、主体の追加属性がある場合には、Authenticationのprincipal属性に格納すればよい。

そして主体情報はユーザ認証とアクセス制御の双方で利用することを目的として、SecurityContextに格納される。もちろん、ほかのAPのクラスからSecurityContextの主体情報を参照することも可能だ。

ユーザ認証

次にユーザ認証のクラス構造を説明する。

図3: クラス図(ユーザ認証) ※ 文字を認識できるjpg画像がサンプルに含まれているのでそちらをご覧になってほしい

ユーザ認証ロジックはAuthenticationManagerのauthenticateに実装される。

HTTPタグを利用した際には、Filterからこのメソッドが呼びだされる。

AuthenticationManagerの実装クラスであるProviderManagerは複数のAuthenticationProviderにユーザ認証処理を委譲する仕組みを持っているので、RDBMSに主体情報を格納する方式とLDAPに主体情報を格納する方式などを併用することができる。

DaoAuthenticationProviderは、UserDetailsServiceから主体情報を取得し、妥当性検証をするクラスである。UserDetailsServiceはusernameを条件にUserDetailsを実装したAP固有の主体情報を返却する。

UserDetailsはこのAP固有の主体情報とSpring Security形式の主体情報の橋渡しをしてくれるインタフェースだ。

DaoAuthenticationProviderはAP固有の主体情報からUserDetailsが提供するアクセッサを利用してAuthenticationを構築するのである。

ユーザ認証が成功した場合、AuthenticationManagerからAuthenticationが返却される。

デフォルトのFilterにユーザ認証を任せているときには、意識する必要はないが、独自のロジック内でauthenticateを呼び出した際には、SecurityContextにAuthenticationを格納することを忘れないように注意する必要がある。

以上がユーザ認証のクラス構造のおおよその説明だが、主体情報に追加属性を保持したい場合には、次のような拡張をすることになる。

この例では主体情報の取得元として、MySQLの次のようなテーブルを利用している。

表1: usersテーブル

列名
username VARCHAR(20)
password VARCHAR(20)
company VARCHAR(20)
division VARCHAR(50)

表2: authorities

列名
username VARCHAR(20)
authority VARCHAR(10)

ソースコード8: MyUser.java抜粋(全サンプルのダウンロード)

/**
 * 会社名と部署名を保持する主体情報クラス
 * Spring Securityがデフォルトで提供するUserクラスを拡張している。
 */
public class MyUser extends User {

private static final long serialVersionUID = 4387960616525114229L;
private final String companyName;
private final String divisionName;

public MyUser(String username, String password, String companyName,
    String divisionName, boolean enabled, boolean accountNonExpired,
    boolean credentialsNonExpired, boolean accountNonLocked,
    Collection<GrantedAuthority> authorities) {
      super(username, password, enabled, accountNonExpired,
                          credentialsNonExpired, accountNonLocked, authorities);
      this.companyName = companyName;
      this.divisionName = divisionName;
  }

  public String getCompanyName() { return this.companyName; }
  public String getDivisionName() { return this.divisionName; }
}

ソースコード8: MyUserDetailsService.java抜粋(全サンプルのダウンロード)

/**
 * MyUserに対応したUserDetailsServiceクラス
 * Spring Securityがデフォルトで提供するRDBMSから主体情報を取得するJdbcDaoImplを拡張している。
 */
public class MyUserDetailsService extends JdbcDaoImpl {

@Override
  protected List<UserDetails> loadUsersByUsername(String username) {
    return getJdbcTemplate().query(getUsersByUsernameQuery(), new String[] { username },
new RowMapper<UserDetails>() {
             public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
                String username = rs.getString(1);
                String password = rs.getString(2);
                boolean enabled = rs.getBoolean(3);
                String company = rs.getString(4);
                String division = rs.getString(5);
                return new MyUser(username, password, company, division, enabled, true, true, true,
                                                                  AuthorityUtils.NO_AUTHORITIES);
              }
            });
  }

@Override
  protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
                                                          List<GrantedAuthority> combinedAuthorities) {
    UserDetails user = super.createUserDetails(username, userFromUserQuery, combinedAuthorities);

    if (userFromUserQuery instanceof MyUser) {
      MyUser myUser = (MyUser) userFromUserQuery;
      return new MyUser(user.getUsername(), user.getPassword(), myUser.getCompanyName(),
        myUser.getDivisionName(), user.isEnabled(), user.isAccountNonExpired(), 
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
    } else {
       return user;
    }
  }
}

ソースコード8: applicationContext-security.xml抜粋(全サンプルのダウンロード)

<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_ADMIN" />
</http>

<authentication-manager>
<!-- providerとしてmyUserDetailsServiceを指定 -->
<authentication-provider user-service-ref="myUserDetailsService">
</authentication-provider>
</authentication-manager>

<!-- JdbcDaoImplを拡張したUserDetailsServiceの実装クラスの設定 -->
<beans:bean id="myUserDetailsService" 
class="sample.web1.security.MyUserDetailsService">
<beans:property name="dataSource" ref="dataSource" />
<beans:property name="usersByUsernameQuery"
value="select username,password,'true',company,division from users 
where username = ?" />
<beans:property name="authoritiesByUsernameQuery"
    value="select username,authority from authorities where username = ?" />
</beans:bean>

<beans:bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="com.mysql.jdbc.Driver" />
<beans:property name="url"
    value="jdbc:mysql://localhost:3306/security-sample" />
<beans:property name="username" value="root" />
<beans:property name="password" value="root" />
</beans:bean>

アクセス制御

最後にアクセス制御のクラス構造について説明する。ここではMethod Securityのクラス構造を示している。

図4: クラス図(アクセス制御) ※ 文字を認識できるjpg画像がサンプルに含まれているのでそちらをご覧になってほしい

中核となるAbstractSecurityInterceptorのbeforeInvocationには次の3ステップのアクセス制御処理が実装されている。

  1. SecurityContextHolderから主体情報を取得する
  2. MethodSecurityMetadataSourceから当該メソッドへのアクセスに必要な権限リストを取得する
  3. AccessDecisionManagerに1、2で取得した情報を渡して、アクセス可否を判断する

AccessDecisionManagerは複数のVoter(投票者)の投票結果により、アクセス可否を判断するが、次の3種類の実装が用意されている。

  1. AffirmativeBased: Voterのうち1つでも賛成すればアクセスを許可する。ネガティブ・コンセンサス方式。
  2. UnanimousBased: Voterのうち1つでも反対すればアクセスを拒否する。全会一致方式。
  3. ConsensusBased: Voterの賛成票と反対票の数を比較し、多い方に従う。多数決方式。

AccessDecisionManagerはVoterを複数保持することで、2つ以上のアクセス制御ロジックを並存させることができる。

もしアクセス制御ロジックを拡張したい場合には、新たにVoterを実装すればよい。たとえば主体の権限に基づくアクセス制御に加えて、システム時間によってアクセスできるURLを制限する場合には次のような実装をすることになる。

ソースコード9: MyVoter.java抜粋(全サンプルのダウンロード)

/**
 * ロールTIME_CHECKを要求するセキュアオブジェクトに対して、時間帯チェックを    実施する クラス
 * システム時間がFROMとTOに指定された時間に含まれるとき、賛成票を投じる。
 */
public class MyVoter extends RoleVoter {
private int from;
private int to;

  public int getFrom() { return from; }
  public void setFrom(int from) { this.from = from; }
  public int getTo() { return to; }
  public void setTo(int to) { this.to = to; }

  public MyVoter() {
    setRolePrefix("TIME_CHECK");
  }

  @Override
  public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
    int result = ACCESS_ABSTAIN;

    for (ConfigAttribute attribute : attributes) {
      if (this.supports(attribute)) {
        result = ACCESS_DENIED;

        int nowHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

        if (getFrom() <= nowHour && nowHour < getTo()) {
          return ACCESS_GRANTED;
        }
      }
    }
    return result;
  }
}

ソースコード9: applicationContext-security.xml抜粋(全サンプルのダウンロード)

<!-- 独自のVoterを利用するため、accessDecisionManagerを明示的に指定する -->
<http auto-config="true" access-decision-manager-ref="accessDecisionManager">
<intercept-url pattern="/**" access="ROLE_ADMIN,TIME_CHECK" />
</http>

<authentication-manager>
<authentication-provider>
<user-service>
<user name="user" password="useruser" authorities="ROLE_USER" />
<user name="admin" password="adminadmin" 
authorities="ROLE_USER,ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>

<beans:bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.UnanimousBased">
<beans:property name="decisionVoters">
<beans:list>
<!-- VoterとしてmyVoterを指定 -->
<beans:ref bean="myVoter"/>
<beans:ref bean="roleVoter" />
</beans:list>
</beans:property>
</beans:bean>

<beans:bean id="roleVoter"
class="org.springframework.security.access.vote.RoleVoter" />

<!-- 時間帯チェッククラス
Fromには9時をToには18時を指定している -->
<beans:bean id="myVoter" class="sample.web1.security.MyVoter">
<beans:property name="from" value="9" />
<beans:property name="to" value="18" />
</beans:bean>

*  *  *

ここまで基本的な機能を見てきたが、Spring Securityは、ほかにもユーザ認証でOpenIDを利用したり、アクセス制御で、どの主体がどのデータにアクセスできるかを制御したりする機能なども提供している。

残念ながら、すべての機能を紹介することはできなかったが、その他の機能についてはSpring Security公式サイトのリファレンスを参照してほしい。

Spring Securityを利用することで、ユーザ認証やアクセス制御の実装方法で悩むことは格段に少なくなるはずだ。これまでAPごとに毎回、独自のユーザ認証・アクセス制御処理を実装していた人は、これを機会にSpring Securityに乗り換えてみてはいかがだろうか。

執筆者紹介

市川照晃(ICHIKAWA Teruaki) - CSKシステムズ


Javaや.NETのSI向けフレームワークの開発や導入支援、社内教育などに従事する。

尊敬する人は戦国四君の一人、孟嘗君と諸子百家のうち法家思想の大家、韓非。座右の銘は『人知らずして慍(うら)みず(論語)』。中国思想、中国史、法律、政治、経済などを経て、情報技術に興味を持ち今に至る。

近年は投資と家事の学習に注力する。