今回は、Spring MVC上でSpring Securityを利用して、独自ログイン画面による認証処理を実装してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
流用ソース
Spring Bootを使っていた、下記記事のソースコードを流用するものとする。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、「前提条件」のプログラムから変更したプログラムである。
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>
また、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://(サーバー名):(ポート番号)/(プロジェクト名)/」とアクセスすると、以下のログイン画面が表示される
2) security-context.xmlに定義したユーザーと違うユーザー名またはパスワードを入力し、「ログイン」ボタンを押下
3) 以下のように、ログインができず、ログイン画面にエラーメッセージが表示される
4) security-context.xmlに定義したユーザー同じユーザー名・パスワードを入力し、「ログイン」ボタンを押下
5) 以下のように、ログインでき、検索画面が表示されることが確認できるので、「ログアウト」ボタンを押下
要点まとめ
- 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}”/>」というタグを追加する必要がある。