なみひらブログ

学んだことを日々記録する。~ since 2012/06/24 ~

Spring Securityを適用するときの作業メモ

背景

Springプロジェクトが提供する認証の機構「Spring Security」を適用したときのメモです。
今回はログイン・ログアウト・ロールに合わせた画面表示切り替えあたり処理を書いてみます。

環境

前提として以下を利用して書いています。

メモ

 必要なライブラリを取り込む

  • ビルドファイルに定義を記載する
  • 最低限必要なライブラリは「spring-security-web」「spring-security-config」で、「spring-security-taglibs」は画面側で画面切り替えのときに使う「sec:authorize」とかを使うときに必要。
  • Springとは別のバージョン管理されている
    • spring 4.2.4.RELEASE vs spring-security 4.1.2.RELEASE(2016/08/16時点最新)
  • pom.xml
    • バージョン情報はパラメータ化する書き方は必須ではない(;´Д`)
<project>

  <properties>
    (略)
     <org.springframework.security.version>4.1.2.RELEASE</org.springframework.security.version>
  </properties>

  <dependencies>
    (略)
    <!-- Security -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${org.springframework.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>${org.springframework.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-taglibs</artifactId>
      <version>${org.springframework.security.version}</version>
    </dependency>    
  </dependencies>
</project>

 Spring-Securityの設定を書く

設定の書き方はJava Config(Javaクラスで表現する)とXML Config (XMLファイルで書く)がありますが、今回はXMLで記載します*1

web.xml
  • 設定をロードするようにする
  • 設定ファイル(spring-security.xml)は/WEB-INF/spring/に配置している想定。
    • (別件)param-valueが複数行で多重定義できることを今回知った(;´Д`)一応、/WEB-INF/spring/*.xmlと一行でも書ける。
  • 以下※はコメント。
(中略)
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/spring/spring-context.xml
      /WEB-INF/spring/spring-security.xml     ※今回追加
    </param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> ※Spring動かす上で元々あると思われる。  
  </listener>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>      ※今回追加。名前は固定。
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>    ※全部のパスに適用する。
  </filter-mapping>
spring-security.xml
  • permitAllとかhasRoleとかはSpringELと呼ばれるSpring固有の記法。
  • permitAllとかは先に定義したほうが優先度が高い。
  • security="none"とaccess="permitAll"は意味的には同義だが、noneだとSpringSecurity関連の処理が一切されない。一方permitAllは処理はするが必ず認証OKになるイメージ。
    • 振る舞いの違いの例としては、後述のJSP側でのCSRFのための「<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>」と記載するが、これらの$値はSpringSecuriyが生成するため、「pattern="/login" security="none"」と書くとname/value値が空になる(;´Д`)この場合はpermitAllで書く必要があり。
    • ただしテンプレートエンジンにJSPではなくthymeleaf(HTML)を使う場合thymeleaf側で上記コードを自動挿入してくれるため、どっちでもよくなる(´Д`)
    • 他の例では、permitAllを指定すると、GET以外のリクエストについてpermitAllを指定してもCSRF検証が行われるので結構に邪魔になる(;´Д`)ajaxで叩く時とかでもCSRF情報を付与する必要がある。そういうときはnoneで指定したほうが楽できる。
  • 以下※はコメント。
<beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:sec="http://www.springframework.org/schema/security"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                   http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                   http://www.springframework.org/schema/security
                   http://www.springframework.org/schema/security/spring-security.xsd">

  <sec:http pattern="/resources/**" security="none"/>   ※js,cssが配置されているパスは適用外にする。

  <sec:http auto-config="false" use-expressions="true" entry-point-ref="loginUrlAuthenticationEntryPoint">
    <sec:intercept-url pattern="/login" access="permitAll"/>  ※ログイン画面は全員アクセスできる。
    <sec:intercept-url pattern="/logout" access="permitAll"/> ※ログアウト画面も全員アクセスできる。
    <sec:intercept-url pattern="/**" access="hasRole('USER')" /> ※他の画面はUSERロールをもっていないとアクセスできない。

    <sec:custom-filter position="FORM_LOGIN_FILTER" ref="applicationUsernamePasswordAuthenticationFilter" /> ※ログインフォーム向けにある既存の認証処理を独自認証処理に置き換える。
    
    <sec:logout logout-url="/logout" logout-success-url="/view/login?from=logout" invalidate-session="true" /> ※ログアウトに関する設定。
    
    <sec:csrf/> ※POST時にCSRFの検証も行う。処理自体は自動で行われる。
  </sec:http>

  <bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg value="/login" />  ※ログイン画面の指定。認証なしでアクセスするとここに飛ばされる。
  </bean>

  <bean id="applicationUsernamePasswordAuthenticationFilter" class="jp.co.example.filter.ApplicationUsernamePasswordAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" /> ※認証成功したあとに処理するハンドラーの指定。
    <property name="authenticationFailureHandler" ref="authenticationFailureHandler" /> ※認証失敗したあとに処理するハンドラーの指定。
    <property name="filterProcessesUrl" value="/authentication" />  ※認証を行うパス。ここに認証情報をつけてPOSTすると認証処理(filter)が実行される。
  </bean>

  <bean id="authenticationSuccessHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler">
    <property name="defaultTargetUrl" value="/home"/> ※認証成功した後の遷移先。↑ハンドラー自体は既存のものを利用。
  </bean>

  <bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="/login?error=true"/> ※認証失敗した後の遷移先。↑ハンドラー自体は既存のものを利用。
  </bean>

  <sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="applicationUsernamePasswordAuthenticationProvider" /> ※認証されたユーザの情報を提供するプロバイダの指定。
  </sec:authentication-manager>

  <bean id="applicationUsernamePasswordAuthenticationProvider" class="jp.co.example.auth.ApplicationUsernamePasswordAuthenticationProvider">
  </bean> ※認証通過時、認証ユーザの提供するプロバイダ。内容は後述。
   
</beans>

 ログイン画面を作る

上記設定でも記載した/loginの実装例。

  • コントローラ
    • 基本画面を返すだけ。ただしクエリによって出し分けたい要素があるためそのあたりの処理も入っている(error、from)。
    • LoginViewController.java
import static org.springframework.web.bind.annotation.RequestMethod.*;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * login controller
 */
@Controller
public class LoginViewController {

    @RequestMapping(value = "/login", method = GET)
    public String get(final Model model, 
            @RequestParam(required = false, defaultValue = "") final String error,
            @RequestParam(required = false, defaultValue = "") final String from) {
        model.addAttribute("isError", !error.isEmpty());
        model.addAttribute("fromLogout", from.contains("logout"));
        return ".login";
    }

}
  • 画面キャプチャは割愛(;´Д`)
  • submitすると上記で指定したパス/authenticationにPOSTする。
  • login.jsp
<div class="row">
  <h2 class="text-center">ユーザー情報を入力してください</h2>
  <div class="well">
    <c:if test="${isError}">
      <div class="alert alert-danger alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <strong>Login Error</strong>
      </div>
    </c:if>
    <c:if test="${fromLogout}">
      <div class="alert alert-success alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <strong>Success logout</strong>
      </div>
    </c:if>
    <form class="form center-block" action="/app/authentication" method="post">  ※設定したパスにPOSTする。
      <div class="form-group">
        <input type="text" class="form-control input-lg" name="username" placeholder="username">
      </div> 
      <div class="form-group">
        <input type="password" class="form-control input-lg" name="password" placeholder="password">
      </div>
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> ※CSRF対策が入っているため入れる。
      <div class="form-group">
        <button type="submit" class="btn btn-primary btn-lg btn-block">ログイン</button>
      </div>
    </form>
  </div>
</div>

 認証情報の検証処理を書く

フォーム画面からの認証情報の検証はfilterが行う。

  • 実行タイミングは今回はFORM_LOGIN_FILTERの位置。
  • ApplicationUsernamePasswordAuthenticationFilter.java
  • 以下※はコメント
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class ApplicationUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        // Obtain info form request
        ※今回使ったUsernamePasswordAuthenticationFilterはPOSTデータの"username""password"というパラメータをとれるようになっている。
        ※他のパラメータ名で処理したい場合やパラメータを追加したい場合は自分で拡張する。
        final String username = super.obtainUsername(request);    ※"username"の値をとる。
        final String password = super.obtainPassword(request);     ※"password"の値をとる。
        if (username == null || password == null) {
            throw new AuthenticationServiceException("Authentication info must be set");
        }        

        // Validate info
        // FIXME
        ※今は固定値(´,,・ω・,,`)DBに接続しても外のAPIにアクセスして検証してもよい。
        if (!username.equals("namihira") || !password.equals("namihira")) {
            throw new AuthenticationServiceException("Authentication Error");
        }
        
        // Create token form input
        final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
        
        // Move to identify user phase
        return this.getAuthenticationManager().authenticate(authToken);
    }
    
}

 認証されたユーザの情報を構築する

上記で認証検証処理を追加したユーザについて、ユーザ情報を構築し返却する。

  • ApplicationUsernamePasswordAuthenticationProvider.java
  • 以下※はコメント。
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import jp.co.example.dto.UserDto;

/**
 * Authentication Provider for App
 */
public class ApplicationUsernamePasswordAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // Identify user
        final String username = authentication.getName();
        final String password = authentication.getCredentials().toString();
        final UserDto user = new UserDto();
        user.setUserId(username);
        user.setPassword(password);
        
        // Create authentication token
        return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), 
                Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));    ※認証されたユーザにUSERロールを付与する。
    }

    @Override
    public boolean supports(Class<?> authentication) {    ※このプロバイダーが提供する型の指定。ユーザ情報でfalseが返ると認証エラーになる。なにに使うか分かっていない(´,,・ω・,,`)
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }

}

ロールによって画面の表示内容を変える

SpringSecurityのタグを使って、画面の表示内容を変えてみる。
今回はbootstrapでのナビゲーションのリンクの表示非表示のみ

  • navigation.jsp
  • 以下※はコメント
(略)
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>   ※タグの宣言がいる。
(略)
<nav class="navbar navbar-inverse">
  <div class="container-fluid">
    (略)
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <sec:authorize access="hasRole('USER')">  ※"ROLE_USER"が付与されたユーザのみ表示される。ROLE_USER→USERとマッピングされる。
          <ul class="nav navbar-nav">
            <li><a href="/app/usersetting">個人設定</a></li>
          </ul>
      </sec:authorize>
 (略)
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
(略)

ログアウト処理を入れる

ログイン状態を解除するログアウト処理を入れてみる。
先述したspring-security.xmlにlogoutの設定を入れたので、設定のとおりに/logoutにPOSTするとログアウトされる。

  • logout.jsp
  • 以下※はコメント
(略)
  <h2 class="text-center">ログアウトしますか?</h2>
  <div class="well">
    <form class="form-inline" action="/app/logout" method="post">  ※指定したところにPOSTする。
      <div class="text-center">
        <input type="submit" class="btn btn-primary btn-lg" value="Logout" />
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>   ※CSRF対策を入れているので必要。
      </div>
    </form>
  </div>

まとめ

裏ルールが結構ある(;´Д`)

参考

*1:最近はJava Configのほうが主流になりつつある(;´Д`)