0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Spring Boot】Spring Security 認可制御(メソッド指定・パス変数指定など)

Last updated at Posted at 2021-07-26

はじめに

Spring Security の認可のうち、メソッドへの認可及びパス変数に基づく認可について、備忘としてまとめておきます。
HTTP リクエスト(Web リソース)からの認可制御や、認可エラー画面(403エラー画面)のカスタマイズについても、併せて残しておきます。

<参考サイト・書籍>
Spring Security リファレンス
『Spring 徹底入門』株式会社NTTデータ著(翔泳社・2016年7月刊)

1. メソッド実行前の認可制御の設定

メソッド実行前の認可制御から確認していきます。

まず、次のように @EnableGlobalMethodSecurity アノテーションを使用して、メソッドに対する認可処理を有効にする必要があります。
これは、コンフィグレーションクラス(WebSecurityConfigurerAdapter を拡張したクラス)に設定します。

SecurityConfig.java
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 略
}

定義式(Expression)部分に、prePostEnabled = true とすることにより、以下で使用する @PreAuthorize などのアクセス権設定のためのアノテーションが使用できるようになります。

1-1. メソッドの引数(パス変数)を使用して指定

1-1-1. 基本的な指定

次のように、コントローラーのメソッドに、@PreAuthorize アノテーションを使用してアクセス権を定義します。

UserController.java
// 略
@Controller
@RequestMapping
public class UserController {
    // 略
    @PreAuthorize("#username == authentication.principal.username")
    @GetMapping("/users/{username}")
    public String userInfo(@PathVariable String username, Model model) {
        // 略
    }
}

定義式 ("#username == authentication.principal.username") のうち、#username は、userInfo メソッドの引数 String username に対応しています。
authentication.principal.username は、現在ログイン中のユーザー名を表しています。これは、authentication.name 又は principal.username と書いても構いません。

これは、自身のユーザー名がパス変数に指定されている場合のみアクセスを認可する例です。

1-1-2. 一対多の関係にある場合の指定

次に、User エンティティーと Message エンティティーが、一対多の関係にある場合の例を書くと次のようになります。
これは、User は自身に紐付いている Message に対してのみ、データの更新が許可される例です。

MessageController.java
// 略
@Controller
@RequestMapping("/message")
public class MessageController {
    // 略
    @PreAuthorize("#message.user.username == authentication.name")
    @PostMapping("/edit/{id}")
    public String updateMessage(@Valid Message message, BindingResult result, @PathVariable Integer id) {
        // 略
    }
}

1-2. ユーザーのROLE(権限情報)を使用して指定

以下は、ユーザーの有する ROLE(権限情報)に基づきアクセス権を設定する例です。

UserController.java
// 略
@Controller
@RequestMapping
public class UserController {
    // 略
    @PreAuthorize("hasRole('USER')")
    @GetMapping("/users/{username}")
    public String userInfo(@PathVariable String username, Model model) {
        // 略
    }
}

定義式(Expression)の ("hasRole('USER')") で、ROLE(権限情報) に USER を有しているユーザーのみにアクセス権を認可するようにしています。
この設定の場合は、USER 権限を有しているユーザーは全ての username についての URL が参照できることになります。

1-3. 定義用のメソッド(Been)を作成して指定

メソッドの引数だけでは設定が難しい内容は、次のように専用のメソッド(Been)を作成して定義を行います。

①コントローラー
ここでも、@PreAuthorize アノテーションを使用して定義します。

UserController.java
// 略
@Controller
@RequestMapping
public class UserController {
    // 略
    @PreAuthorize("@webSecurity.checkUserId(authentication, #id)")
    @GetMapping("/user/{id}")
    public String userPage(@PathVariable Integer id, Model model) {
        // 略
    }
}

定義内容は、@webSecurity.checkUserId(authentication, #id)の部分です。
構文としては、次のようになっています。

@クラス名.メソッド名(引数...)

②定義用メソッド
次のような専用のクラス(WebSecurity.java)を作成して、認可を行うメソッドを定義します。
メソッドの内容まで書いておきましたが、これはあくまで一例です。必要に応じて、適宜作成することになります。

WebSecurity.java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

@Component
public class WebSecurity {
    
    @Autowired
    private UserDetailsServiceImpl userDetailsServiseImpl;
    
    public boolean checkUserId(Authentication authentication, int id) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = userDetailsServiseImpl.getUserByUsername(userDetails.getUsername());
        return id == user.getId();
    }
}

<参考サイト>
Web セキュリティ式で Bean を参照する
Spring SecurityのPreAuthorizeで独自メソッドを呼び出す

1-4. 条件を組み合わせて指定

以上で見てきた内容は、組み合わせて定義することもできます。
例えば、次のような感じです。

Sample.java
    @PreAuthorize("hasRole('ADMIN') or #username == principal.username")
Sample.java
    @PreAuthorize("hasRole('ADMIN') or @webSecurity.checkUserId(authentication, #id)")

2. メソッド実行後の認可制御の設定

次に、メソッド実行後の認可制御です。
この場合も @EnableGlobalMethodSecurity アノテーションを使用して、メソッドに対する認可処理を有効にしておきます。
これにより、@PostAuthorize アノテーションが使用できるようになります。

SecurityConfig.java
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 略
}

2-1. メソッドの戻り値を使用して指定

メソッド実行後の認可制御には、@PostAuthorize アノテーションを使用します。

UserDetailsServiceImpl.java
package com.example.demo;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PostAuthorize;
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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserDao userDao;
    // 略
    @PostAuthorize("returnObject.username == principal.username")
    @Transactional(readOnly = true)
    public User findUser(Integer userId) {
        User user = userDao.findById(userId).orElseThrow(RuntimeException::new);
        return user;
    }
    // 略
}

定義式(Expression)は、"returnObject.username == principal.username"の部分です。
メソッドの戻り値は、returnObject で取得することができます。

3. HTTP リクエストに対する認可制御

次に、HTTP リクエスト(Web リソース)から認可制御を行う場合です。
定義する内容は、「1. メソッド実行前の認可制御の設定」で記載した内容と基本的に同じです。

3-1. メソッドの引数を使用して指定

Web リソースに対する認可は、次のように、コンフィギュレーションクラスの HttpSecurity クラスの設定のところに定義します。

①コンフィギュレーションクラス

SecurityConfig.java
// 略
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/signup").permitAll()
            .antMatchers("/users/{username}/**").access("#username == principal.username")
            .anyRequest().hasAnyRole("ADMIN", "USER").and()
            .formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll().and()
            .logout().permitAll().and()
            .exceptionHandling().accessDeniedPage("/accessDenied");
    }
}

定義している部分は、次の1行です。

Sample.java
    .antMatchers("/users/{username}/**").access("#username == principal.username")

principal.username で現在ログイン中のユーザー名を取得しています。
#username には変数が入ります。これは、次の「②コントローラー」のメソッドの引数が充てられることになります。

②コントローラー
該当するコントローラーは次のとおりです。
ここのメソッドの引数 String username が、上記のコンフィギュレーションクラスの #username に充てられることになります。

UserController.java
// 略
@Controller
@RequestMapping
public class UserController {
    // 略
    @GetMapping("/users/{username}")
    public String userInfo(@PathVariable String username, Model model) {
        // 略
    }
}

3-2. 定義用のメソッド(Been)を作成して指定

Web リソースに対する認可でも、専用のメソッドを作成して定義することができます。

①コンフィギュレーションクラス

SecurityConfig.java
// 略
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/signup").permitAll()
            .antMatchers("/message/edit/{messageId}/**").access("@webSecurity.checkMessageId(authentication, #messageId)")
            .anyRequest().hasAnyRole("ADMIN", "USER").and()
            .formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll().and()
            .logout().permitAll().and()
            .exceptionHandling().accessDeniedPage("/accessDenied");
    }
}

定義している部分は、次の1行です。

Sample.java
    .antMatchers("/message/edit/{messageId}/**").access("@webSecurity.checkMessageId(authentication, #messageId)")

@webSecurity クラスの checkMessageId メソッドで、2つの引数(authentication, #messageId)を使用して定義しています。

②定義用メソッド

定義用のメソッド(Been)は次のとおりです。クラスファイル(WebSecurity.java)を一つ作成しています。
メソッドの内容はあくまで一例です。必要に応じて、適宜作成してください。

WebSecurity.java
// 略
@Component
public class WebSecurity {
    
    @Autowired
    private MessageService messageService;
    
    public boolean checkMessageId(Authentication authentication, Integer messageId) {
        Message message = messageService.getMessage(messageId);
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername().equals(message.getUser().getUsername());
    }
}

<参考サイト>
SpringSecurityでEL式を拡張してカスタムルールを作るときのメモ

3-3. 条件を組み合わせて指定

Web リソースに対する認可でも、次のように幾つかの条件を組み合わせて認可の定義をすることができます。

Sample.java
    .antMatchers("/message/edit/{messageId}/**").access("hasRole('ADMIN') or @webSecurity.checkMessageId(authentication, #messageId)")

4. 認可エラー時のレスポンス画面のカスタマイズ

アクセスが拒否された場合は、HTTP403 エラーが生じます。
デフォルトでは、次のような画面が表示されます。
2021-07-27 222538.png
ここで独自のエラー画面を表示するには、以下のように設定を行います。

①コンフィギュレーションクラス
コンフィグレーションクラスでは、次のように定義します。

SecurityConfig.java
// 略
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.// 略
            .exceptionHandling().accessDeniedPage("/accessDenied");
    }
}

関係部分は、次の1行です。
ここで、カスタム画面のURL "/accessDenied" を指定しています。

Sample.java
    http.// 略
        .exceptionHandling().accessDeniedPage("/accessDenied");

②HTMLファイル
適当ですが、次のように作成しておきました。

src/main/resources/templates/accessDenied.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>アクセスエラー</title>
</head>
<body>
    <h2>アクセスエラー</h2>
    <a href="/">トップページ</a></body>
</body>
</html>

③コントローラー
コントローラーでも、リクエストURL /accessDenied に対応する HTML ファイルを指定しておきます。

Sample.java
    // 略
    @GetMapping("/accessDenied")
    private String accessDenied() {
        return "accessDenied";
    }
    // 略

設定は以上です。

アクセス拒否がされた場合に、次のように表示されれば成功です。
2021-07-27 222712.png

さいごに

書籍を読んだだけでは、なかなか理解できなかったため、実際に確認した内容を備忘として残しておきました。
何らかのご参考となれば幸いです。

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?