Spring Boot セキュリティ関連

Spring Securityで権限制御を実装してみた

今回は、Spring Securityで一般ユーザーと管理者ユーザーを用意し、管理者ユーザーのみアクセスできる画面の制御を実装してみたので、そのサンプルプログラムを共有する。

前提条件

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

IntelliJ IDEA上でGradleを使ってWeb画面のSpring Bootプロジェクトを作成してみたSpring Bootのプロジェクトを新規作成を「IntelliJ IDEA」のメニューから実施しようとしたところ、無料の「Commun...

また、以下の構成のuser_passテーブルが作成されていること。
user_pass_desc

参考ソース

Spring Securityの実装は、下記記事の実装も参照のこと。

Spring Boot上でSpring Securityのエラー処理を追加してみた今回は、Spring Securityでエラーが発生した場合に、エラー画面に遷移する処理を追加してみたので、そのサンプルプログラムを共有...

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

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

Spring Securityの設定ファイルの内容は以下の通りで、管理者ユーザー向けのアクセスパス「/has_admin_auth」を、管理者権限(ADMIN)をもつユーザーのみアクセス可能にしている。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.firewall.DefaultHttpFirewall;

@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * Spring-Security用のユーザーアカウント情報を
     * 取得・設定するサービスへのアクセス
     */
    @Autowired
    private UserPassAccountService userDetailsService;

    @Override
    public void configure(WebSecurity web) {
        //org.springframework.security.web.firewall.RequestRejectedException:
        //The request was rejected because the URL contained a potentially malicious String ";"
        //というエラーログがコンソールに出力されるため、下記を追加
        DefaultHttpFirewall firewall = new DefaultHttpFirewall();
        web.httpFirewall(firewall);
        //セキュリティ設定を無視するリクエスト設定
        //静的リソースに対するアクセスの場合は、Spring Securityのフィルタを通過しないように設定する
        web.ignoring().antMatchers("/css/**");
    }

    /**
     * SpringSecurityによる認証を設定
     * @param http HttpSecurityオブジェクト
     * @throws Exception 例外
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //初期表示画面を表示する際にログイン画面を表示する
        http.formLogin()
                //ログイン画面は常にアクセス可能とする
                .loginPage("/login").permitAll()
                //ログインに成功したら検索画面に遷移する
                .defaultSuccessUrl("/")
                .and()
                //ログイン画面のcssファイルとしても共通のdemo.cssを利用するため、
                //src/main/resources/static/cssフォルダ下は常にアクセス可能とする
                .authorizeRequests().antMatchers("/css/**").permitAll()
                .and()      //かつ
                //管理者ユーザー向けのアクセスパスは、管理者権限をもつユーザーのみアクセス可能にする
                //それ以外はログインした全てのユーザーがアクセス可能にする
                .authorizeRequests().mvcMatchers("/has_admin_auth").hasAuthority("ADMIN")
                .anyRequest().authenticated()
                .and()    //かつ
                //ログアウト時はログイン画面に遷移する
                .logout().logoutSuccessUrl("/login").permitAll()
                .and()    //かつ
                //エラー発生時はエラー画面に遷移する
                .exceptionHandling().accessDeniedPage("/toError");
    }

    /**
     * 認証するユーザー情報をデータベースからロードする処理
     * @param auth 認証マネージャー生成ツール
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //認証するユーザー情報をデータベースからロードする
        //その際、パスワードはBCryptで暗号化した値を利用する
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * パスワードをBCryptで暗号化するクラス
     * @return パスワードをBCryptで暗号化するクラスオブジェクト
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}



また、コントローラクラスの内容は以下の通りで、ログイン画面に遷移する際に、一般ユーザーと管理者ユーザーを登録する仕様になっている。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DemoController {

    /**
     * Spring-Security用のユーザーアカウント情報を
     * 取得・設定するサービスへのアクセス
     */
    @Autowired
    private UserPassAccountService userDetailsService;

    /**
     * パスワードをBCryptで暗号化するクラスへのアクセス
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * ログイン画面に遷移する
     * @return ログイン画面へのパス
     */
    @GetMapping("/login")
    public String login(){
        //ユーザーパスワードデータテーブル(user_pass)へユーザー情報を登録する。
        //その際、パスワードはBCryptで暗号化する
        userDetailsService.registerUser("user"
                , passwordEncoder.encode("pass"), "USER");
        userDetailsService.registerUser("admin"
                , passwordEncoder.encode("pass"), "ADMIN");
        //ログイン画面に遷移する
        return "login";
    }

    /**
     * 初期表示画面に遷移する
     * @return 初期表示画面へのパス
     */
    @RequestMapping("/")
    public String index(){
        return "index";
    }

    /**
     * 一般ユーザーの画面に遷移する
     * @return 一般ユーザーの画面へのパス
     */
    @GetMapping("/has_user_auth")
    public String has_user_auth(){
        return "user";
    }

    /**
     * 管理者ユーザーの画面に遷移する
     * @return 管理者ユーザーの画面へのパス
     */
    @GetMapping("/has_admin_auth")
    public String has_admin_auth(){
        return "admin";
    }

    /**
     * エラー画面に遷移する
     * @return エラー画面へのパス
     */
    @RequestMapping("/toError")
    public String toError(){
        return "error";
    }

}

このサンプルプログラムを実行すると、一般ユーザーと管理者ユーザーは、user_passテーブルに、以下のように登録される。
user_passテーブルデータ



さらに、user_passテーブルへのアクセスに関連する部分の内容は、以下の通り。

package com.example.demo;

import lombok.Data;

@Data
public class UserPass {

    /**
     * 指定したユーザー名・パスワード・権限をもつUserPassオブジェクトを作成する
     * @param name ユーザー名
     * @param pass パスワード
     * @param auth 権限
     */
    public UserPass(String name, String pass, String auth){
        this.name = name;
        this.pass = pass;
        this.auth = auth;
    }

    /** ユーザー名 */
    private String name;

    /** パスワード */
    private String pass;

    /** 権限 */
    private String auth;

}
package com.example.demo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;

public class UserPassAccount implements UserDetails {

    /** UserPassオブジェクト */
    private UserPass userPass;

    /**
     * Spring-Security用のユーザーアカウント情報(UserDetails)を作成する
     * @param userPass UserPassオブジェクト
     */
    public UserPassAccount(UserPass userPass){
        this.userPass = userPass;
    }

    /**
     * ユーザー権限情報を取得する
     * @return ユーザー権限情報
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList(userPass.getAuth());
    }

    /**
     * パスワードを取得する
     * @return パスワード
     */
    @Override
    public String getPassword() {
        return userPass.getPass();
    }

    /**
     * ユーザー名を取得する
     * @return ユーザー名
     */
    @Override
    public String getUsername() {
        return userPass.getName();
    }

    /**
     * アカウントが期限切れでないかを取得する
     * @return アカウントが期限切れでないか
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * アカウントがロックされていないかを取得する
     * @return アカウントがロックされていないか
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * アカウントが認証期限切れでないかを取得する
     * @return アカウントが認証期限切れでないか
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * アカウントが利用可能かを取得する
     * @return アカウントが利用可能か
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}



package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
public class UserPassAccountService implements UserDetailsService {

    /**
     * ユーザーパスワードデータテーブル(user_pass)へアクセスするマッパー
     */
    @Autowired
    private UserPassMapper userPassMapper;

    /**
     * 指定したユーザー名をもつSpring-Security用のユーザーアカウント情報を取得する
     * @param username ユーザー名
     * @return 指定したユーザー名をもつSpring-Security用のユーザーアカウント情報
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            throw new UsernameNotFoundException("ユーザー名を入力してください");
        }
        //指定したユーザー名をもつUserPassオブジェクトを取得する
        UserPass userPass = userPassMapper.findByName(username);
        if(userPass == null){
            throw new UsernameNotFoundException("ユーザーが見つかりません");
        }
        //指定したユーザー名をもつSpring-Security用のユーザーアカウント情報を取得する
        return new UserPassAccount(userPass);
    }

    /**
     * 指定したユーザー名・パスワードをもつレコードをユーザーパスワードデータテーブル(user_pass)に登録する
     * @param username ユーザー名
     * @param password パスワード
     * @param auth 権限
     */
    @Transactional
    public void registerUser(String username, String password, String auth){
        if(StringUtils.isEmpty(username)
                || StringUtils.isEmpty(password) || StringUtils.isEmpty(auth)){
            return;
        }
        //指定したユーザー名をもつUserPassオブジェクトを取得する
        UserPass userPass = userPassMapper.findByName(username);
        //UserPassオブジェクトが無ければ追加・あれば更新する
        if(userPass == null){
            userPass = new UserPass(username, password, auth);
            userPassMapper.create(userPass);
        }else{
            userPassMapper.update(userPass);
        }
    }
}
package com.example.demo;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserPassMapper {

    /**
     * ユーザーパスワードデータテーブル(user_pass)からユーザー名をキーにデータを取得する
     * @param name ユーザー名
     * @return ユーザー名をもつデータ
     */
    UserPass findByName(String name);

    /**
     * 指定したユーザーパスワードデータテーブル(user_pass)のデータを追加する
     * @param userPass ユーザーパスワードデータテーブル(user_pass)の追加データ
     */
    void create(UserPass userPass);

    /**
     * 指定したユーザーパスワードデータテーブル(user_pass)のデータを更新する
     * @param userPass ユーザーパスワードデータテーブル(user_pass)の更新データ
     */
    void update(UserPass userPass);

}



<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.UserPassMapper">
    <resultMap id="userPassResultMap" type="com.example.demo.UserPass" >
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="pass" property="pass" jdbcType="VARCHAR" />
        <result column="auth" property="auth" jdbcType="VARCHAR" />
    </resultMap>
    <select id="findByName" resultMap="userPassResultMap">
        SELECT name, pass, auth FROM USER_PASS WHERE name = #{name}
    </select>
    <insert id="create" parameterType="com.example.demo.UserPass">
        INSERT INTO USER_PASS ( name, pass, auth )
        VALUES (#{name}, #{pass}, #{auth})
    </insert>
    <update id="update" parameterType="com.example.demo.UserPass">
        UPDATE USER_PASS SET pass = #{pass}, auth = #{auth}
        WHERE name = #{name}
    </update>
</mapper>

また、HTMLファイルの内容は、以下の通り。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link th:href="@{/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="ログイン" />
   </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <form method="get" th:action="@{/has_user_auth}">
        <input type="submit" value="一般ユーザーの画面へ" />
    </form>
    <br/><br/>
    <form method="get" th:action="@{/has_admin_auth}">
        <input type="submit" value="管理者ユーザーの画面へ" />
    </form>
    <br/><br/><br/><br/>
    <form method="post" th:action="@{/logout}">
        <button type="submit">ログアウト</button>
    </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    ここは一般ユーザーの画面です。<br/><br/>
    <input type="button" value="戻る" onclick="history.back();" />
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    ここは管理者ユーザーの画面です。<br/><br/>
    <input type="button" value="戻る" onclick="history.back();" />
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>エラー画面</title>
</head>
<body>
    管理者ページへのアクセス権限はありません。
    <br/><br/>
    <input type="button" value="戻る" onclick="history.back();" />
</body>
</html>

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

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

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

1) Spring Bootアプリケーションを起動し、「http://(サーバー名):(ポート番号)/」とアクセスし、一般ユーザーの「user」でログイン
サンプルプログラムの実行結果_1

2) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下
サンプルプログラムの実行結果_2

3) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
サンプルプログラムの実行結果_3

4) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下
サンプルプログラムの実行結果_4

5) 管理者ユーザーの画面に遷移しようとするが権限が無く、下記エラー画面に遷移する。ここで「戻る」ボタンを押下
サンプルプログラムの実行結果_5

6) 初期表示画面に戻るので、「ログアウト」ボタンを押下
サンプルプログラムの実行結果_6

7) 以下のように、ログイン画面に戻ることが確認できる
サンプルプログラムの実行結果_7

8) ログイン画面で管理者ユーザーの「admin」でログイン
サンプルプログラムの実行結果_8

9) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下
サンプルプログラムの実行結果_9

10) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
サンプルプログラムの実行結果_10

11) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下
サンプルプログラムの実行結果_11

12) 管理者ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
サンプルプログラムの実行結果_12

13) 初期表示画面に戻るので、「ログアウト」ボタンを押下
サンプルプログラムの実行結果_13

14) 以下のように、ログイン画面に戻ることが確認できる
サンプルプログラムの実行結果_14

要点まとめ

  • Spring Securityによる権限制御を行うには、http.authorizeRequests()オブジェクトのmvcMatchersメソッドでアクセスパスを指定し、hasAuthorityメソッドで権限設定を行えばよい。