Spring MVC

Spring MVCでSpring Securityを利用してみた

今回は、Spring MVC上でSpring Securityを利用して、独自ログイン画面による認証処理を実装してみたので、そのサンプルプログラムを共有する。

前提条件

下記記事の実装が完了していること。

Spring MVCでMyBatisのSQLログ出力をしてみた今回は、Spring MVC上のサンプルプログラムで、MyBatisのカスタムSQLログを出力する処理を追加してみたので、そのサンプルプ...

流用ソース

Spring Bootを使っていた、下記記事のソースコードを流用するものとする。

Spring Boot上で独自ログイン画面上でSpring Securityの認証を行ってみた今回は、Spring Securityを利用して、独自ログイン画面を作成した上で、Spring Securityの認証処理を実装してみた...

サンプルプログラムの作成

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、「前提条件」のプログラムから変更したプログラムである。

pom.xmlの内容は以下の通りで、Spring Securityに関するライブラリを追加している。また、Springのバージョンを4.2.1.RELEASEに変更している。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <name>demo</name>
    <packaging>war</packaging>
    <version>1.0.0-BUILD-SNAPSHOT</version>
    <properties>
        <java-version>1.6</java-version>
        <org.springframework-version>4.2.1.RELEASE</org.springframework-version>
        <org.aspectj-version>1.6.10</org.aspectj-version>
        <org.slf4j-version>1.6.6</org.slf4j-version>
    </properties>
    <dependencies>
        <!-- Spring -->
        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>${org.springframework-version}</version>
             <exclusions>
                  <!-- Exclude Commons Logging in favor of SLF4j -->
                  <exclusion>
                       <groupId>commons-logging</groupId>
                       <artifactId>commons-logging</artifactId>
                 </exclusion>
             </exclusions>
        </dependency>
        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-webmvc</artifactId>
             <version>${org.springframework-version}</version>
        </dependency>
		
        <!-- Thymeleaf -->
        <dependency>
             <groupId>org.thymeleaf</groupId>
             <artifactId>thymeleaf-spring3</artifactId>
             <version>3.0.11.RELEASE</version>
        </dependency>
		
        <!-- lombok -->
        <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <version>1.18.10</version>
             <scope>provided</scope>
        </dependency>
		
        <!-- Validator -->
        <dependency>
             <groupId>javax.validation</groupId>
             <artifactId>validation-api</artifactId>
             <version>2.0.1.Final</version>
        </dependency>
        <dependency>
             <groupId>org.hibernate</groupId>
             <artifactId>hibernate-validator</artifactId>
             <version>6.0.17.Final</version>
        </dependency>
		
        <!-- Spring JDBC -->
        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
             <version>${org.springframework-version}</version>
        </dependency>
		
        <!-- MyBatis -->
        <dependency>
             <groupId>org.mybatis</groupId>
             <artifactId>mybatis</artifactId>
             <version>3.4.1</version>
        </dependency>
        <dependency>
             <groupId>org.mybatis</groupId>
             <artifactId>mybatis-spring</artifactId>
             <version>1.3.0</version>
        </dependency>
		
        <!-- AOP -->
        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>${org.springframework-version}</version>
        </dependency>
        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-aop</artifactId>
             <version>${org.springframework-version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2</version>
        </dependency>
        
        <!-- Apache Common JEXL -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-jexl3</artifactId>
            <version>3.0</version>
        </dependency>
        
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>4.2.16.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>4.2.16.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>4.2.16.RELEASE</version>
        </dependency>
				
        <!-- AspectJ -->
        <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjrt</artifactId>
             <version>${org.aspectj-version}</version>
        </dependency>	
		
        <!-- Logging -->
        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
             <version>${org.slf4j-version}</version>
        </dependency>
        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
             <version>${org.slf4j-version}</version>
             <scope>runtime</scope>
        </dependency>
        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-log4j12</artifactId>
             <version>${org.slf4j-version}</version>
             <scope>runtime</scope>
        </dependency>
        <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
             <version>1.2.17</version>
             <exclusions>
                 <exclusion>
                      <groupId>javax.mail</groupId>
                      <artifactId>mail</artifactId>
                 </exclusion>
                 <exclusion>
                      <groupId>javax.jms</groupId>
                      <artifactId>jms</artifactId>
                 </exclusion>
                 <exclusion>
                      <groupId>com.sun.jdmk</groupId>
                      <artifactId>jmxtools</artifactId>
                 </exclusion>
                 <exclusion>
                      <groupId>com.sun.jmx</groupId>
                      <artifactId>jmxri</artifactId>
                 </exclusion>
             </exclusions>
             <scope>runtime</scope>
         </dependency>

         <!-- @Inject -->
         <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <version>1</version>
         </dependency>
				
         <!-- Servlet -->
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>servlet-api</artifactId>
             <version>2.5</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>javax.servlet.jsp</groupId>
             <artifactId>jsp-api</artifactId>
             <version>2.1</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>jstl</artifactId>
             <version>1.2</version>
         </dependency>
	
         <!-- Test -->
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.13.1</version>
             <scope>test</scope>
         </dependency>        
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.9</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>org.test.int1.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
削除または保存していないWordドキュメントの復元方法【4DDiG Windowsデータ復元】ワード(Word)データ等のファイルを誤って削除してしまった場合は、通常はデータの復元ができませんが、4DDiGというソフトウェアを利用...

また、web.xmlの内容は以下の通りで、Spring Securityのフィルタ設定を追加すると共に、今回追加するsecurity-context.xmlを読み込むようにしている。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	
    <!-- リクエストパラメータのエンコーディング指定 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- Spring Securityのフィルタ設定 -->
    <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>

    <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
    <!-- Spring Security用の定義ファイル security-context.xmlを読み込み対象に追加 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/root-context.xml, /WEB-INF/spring/security-context.xml</param-value>
    </context-param>
	
    <!-- Creates the Spring Container shared by all Servlets and Filters -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Processes application requests -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
		
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>



さらに、security-context.xmlの内容は以下の通りで、流用ソースの「DemoSecurityConfig.java」と同じ内容を実装している。

<?xml version="1.0" encoding="UTF-8"?>
<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-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">

    <!-- ログイン画面のcssファイルとしても共通のdemo.cssを利用するため、 -->
    <!-- src/main/webapp/resources/static/cssフォルダ下は常にアクセス可能とする -->
	<sec:http pattern="/resources/static/css/**" security="none"/>
	
    <sec:http>
    	<!-- ログイン画面は常にアクセス可能とする -->
        <sec:intercept-url pattern="/login" access="permitAll" />
        <!-- それ以外の画面は全て認証を有効にする -->
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <!-- ログインに成功したら検索画面に遷移する -->
        <sec:form-login login-page="/login" default-target-url="/" />
        <!-- ログアウト時はログイン画面に遷移する -->
        <sec:logout logout-success-url="/login" />
    </sec:http>
    
    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
            	<!-- ユーザー名「user」、パスワード「pass」が入力されたらログイン可能とする -->
                <sec:user name="user" password="pass" authorities="USER" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
    
</beans>

また、CSSファイルの内容は以下の通りで、ファイルパスを「src/main/webapp/resources/static/css」フォルダ下に変更している。内容は特に変更していない。

.errorMessage{
    color: #FF0000;
}
.fieldError{
    background-color: #FFCCFF;
}

さらに、コントローラクラスには、ログイン画面を表示する以下のメソッドを追加している。

/**
 * ログイン画面に遷移する
 * @return ログイン画面へのパス
 */
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String login(){
    return "login";
}

「@RequestMapping」アノテーションの属性に「method = RequestMethod.GET」を追加することで、ログイン画面を開く場合のみこのメソッドが呼ばれ、postメソッドで実行されるログイン処理ではこのメソッドが呼ばれないようになっている。



また、ログイン画面のHTMLは以下の通りで、流用ソースの「login.html」と同じような内容を実装している。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link th:href="@{/resources/static/css/demo.css}" rel="stylesheet" type="text/css" />
    <title>ログイン画面</title>
</head>
<body>
    <div th:if="${param.error}" class="errorMessage">
                 ユーザー名またはパスワードが誤っています。
    </div>
    <form method="post" th:action="@{/login}">  
       <table border="0">
           <tr>
               <td align="left" valign="top">ユーザー名:</td>
               <td>
                   <input type="text" id="username" name="username" />
               </td>
           </tr>
           <tr>
               <td align="left" valign="top">パスワード:</td>
               <td>
                   <input type="password" id="password" name="password" />
               </td>
           </tr>
       </table>
       <br/><br/>
       <input type="submit" value="ログイン" />
       <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
   </form>
</body>
</html>

上記ソースコードでは、「<input type=”hidden” th:name=”${_csrf.parameterName}” th:value=”${_csrf.token}”/>」というタグを追加しているが、これは、「DemoSecurityConfig.java」では@EnableWebSecurity アノテーションを利用することでCSRF対策が有効になりCSRFトークンが自動生成されていたが、今回はCSRFトークンが自動生成されないため必要になったため、追加している。

なお、CSRF対策については、以下のサイトを参照のこと。
https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/CSRF.html#springsecuritycsrf

他のHTMLファイルにも同様に、formタグ内に1つずつこのタグを追加している。



さらに、検索画面のHTMLの内容は以下の通りで、流用ソースの「search.html」と同じように、ログアウトボタンの実装をしている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
	<link th:href="@{/resources/static/css/demo.css}" rel="stylesheet" type="text/css" />
    <title>index page</title>
</head>
<body>
    <p>検索条件を指定し、「検索」ボタンを押下してください。</p><br/>
    <form method="post" th:action="@{/search}" th:object="${searchForm}">
        <span th:if="*{#fields.hasErrors('fromBirthYear')}"
              th:errors="*{fromBirthYear}" class="errorMessage"></span>
        <span th:if="*{#fields.hasErrors('toBirthYear')}"
              th:errors="*{toBirthYear}" class="errorMessage"></span>
        <table border="1" cellpadding="5">
            <tr>
                <th>名前</th>
                <td><input type="text" th:value="*{searchName}" th:field="*{searchName}" /></td>
            </tr>
            <tr>
                <th>生年月日</th>
                <td><input type="text" th:value="*{fromBirthYear}" size="4"
                           maxlength="4" th:field="*{fromBirthYear}" th:errorclass="fieldError" />年
                    <select th:field="*{fromBirthMonth}" th:errorclass="fieldError">
                        <option value=""></option>
                        <option th:each="item : *{getMonthItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>月
                    <select th:field="*{fromBirthDay}" th:errorclass="fieldError">
                        <option value=""></option>
                        <option th:each="item : *{getDayItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>日~
                    <input type="text" th:value="*{toBirthYear}" size="4"
                           maxlength="4" th:field="*{toBirthYear}" th:errorclass="fieldError" />年
                    <select th:field="*{toBirthMonth}" th:errorclass="fieldError">
                        <option value=""></option>
                        <option th:each="item : *{getMonthItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>月
                    <select th:field="*{toBirthDay}" th:errorclass="fieldError">
                        <option value=""></option>
                        <option th:each="item : *{getDayItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>日
                </td>
            </tr>
            <tr>
                <th>性別</th>
                <td>
                    <select th:field="*{searchSex}">
                        <option value=""></option>
                        <option th:each="item : *{getSexItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>
                </td>
            </tr>
        </table>
        <br/><br/>
        <input type="submit" value="検索" /><br/><br/>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    </form>
    <form method="post" th:action="@{/logout}">
        <button type="submit">ログアウト</button>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    </form>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-mvc-security/demo

サンプルプログラムの実行結果

サンプルプログラムの実行結果は、以下の通り。

1) Spring MVCアプリケーションを起動し、「http://(サーバー名):(ポート番号)/(プロジェクト名)/」とアクセスすると、以下のログイン画面が表示される
サンプルプログラムの実行_1

2) security-context.xmlに定義したユーザーと違うユーザー名またはパスワードを入力し、「ログイン」ボタンを押下
サンプルプログラムの実行_2

3) 以下のように、ログインができず、ログイン画面にエラーメッセージが表示される
サンプルプログラムの実行_3

4) security-context.xmlに定義したユーザー同じユーザー名・パスワードを入力し、「ログイン」ボタンを押下
サンプルプログラムの実行_4

5) 以下のように、ログインでき、検索画面が表示されることが確認できるので、「ログアウト」ボタンを押下
サンプルプログラムの実行_5

6) 以下のように、ログイン画面に遷移することが確認できる
サンプルプログラムの実行_6

要点まとめ

  • Spring MVCプロジェクトでSpring Securityを利用するためには、必要なライブラリを追加し、web.xmlにSpring Securityのフィルタ設定を行うと共に、XMLの定義ファイル内で「sec:http」「sec:authentication-manager」といったタグを利用してSpringSecurityの定義を行う。
  • Spring SecurityのCSRFトークンを利用できるようにするには、HTMLファイル上でformタグ毎に1つ、「<input type=”hidden” th:name=”${_csrf.parameterName}” th:value=”${_csrf.token}”/>」というタグを追加する必要がある。