今回は、Spring Securityで一般ユーザーと管理者ユーザーを用意し、管理者ユーザーのみアクセスできる画面の制御を実装してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
また、以下の構成のuser_passテーブルが作成されていること。

参考ソース
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テーブルへのアクセスに関連する部分の内容は、以下の通り。
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」でログイン

2) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下

3) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下

4) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下

5) 管理者ユーザーの画面に遷移しようとするが権限が無く、下記エラー画面に遷移する。ここで「戻る」ボタンを押下

8) ログイン画面で管理者ユーザーの「admin」でログイン

9) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下

10) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下

11) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下

12) 管理者ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下

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









