Spring BootのウェブアプリケーションのRole(権限・役割)のメモ。
|
Spring Securityにはユーザー毎の権限(Role)を管理する機能がある。
これにより、特定のユーザーしか表示できない画面や処理を実現することが出来る。
Spring Bootで用意されているUserDetailsは権限を(保持・)取得できるようになっており、Controllerやhtml(Thymeleaf)で取得することが出来る。
UserDetailsは、Spring Securityでユーザー情報を管理するクラス(インターフェース)。(Spring Boot 1.5.6、Spring Security 4.2.3)
UserDetailsServiceでUserDetailsのインスタンスを返すよう実装する。
ここで、UserDetailsに権限(Role)をセットする。
Spring Securityでは権限(Role)は文字列で扱うが、普通は権限の種類は決まっているはずなので、列挙型にするのが自然だろう。
package com.example.demo.auth; public enum ExampleRole { ROLE_USER1, ROLE_USER2, ROLE_ADMIN, ROLE_API }
権限名は「ROLE_」で始まるよう名付けておく。
(文字列として扱うときに「ROLE_」が付いていればいいが、列挙子名が違うのもバグの元になると思われるので、列挙子名にも「ROLE_」を付けてある)
参考: yokobonbonさんのSpring SecurityのhasRole(role)に要注意
package com.example.demo.auth; import java.util.Collection; import java.util.EnumSet; import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service;
@Service public class LoginUserService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username == null) { throw new UsernameNotFoundException("empty"); } // 本来ならDBアクセスしてパスワードやロールを取得するところだが、サンプルなのでプログラム直書き String password; Set<ExampleRole> roles; switch (username) { case "hishidama": password = passwordEncoder.encode("hoge"); roles = EnumSet.of(ExampleRole.ROLE_ADMIN, ExampleRole.ROLE_USER1, ExampleRole.ROLE_USER2, ExampleRole.ROLE_API); break; case "aaa": password = passwordEncoder.encode("1"); roles = EnumSet.of(ExampleRole.ROLE_USER1); break; case "bbb": password = passwordEncoder.encode("2"); roles = EnumSet.of(ExampleRole.ROLE_USER2); break; default: throw new UsernameNotFoundException("not found"); } Collection<? extends GrantedAuthority> authorities = roles.stream() .map(ExampleRole::name).map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new User(username, password, authorities); } }
Userクラス(UserDetailsの具象クラス)のコンストラクターの第3引数に権限一覧をセットする。
ちなみに、Java8のStream APIを使って列挙型からGrantedAuthorityクラスに変換している。
メソッド参照「::
」やコンストラクター参照「::new
」に慣れていない人向けにラムダ式で表すと、
Collection<? extends GrantedAuthority> authorities = roles.stream() .map(role -> new SimpleGrantedAuthority(role.name())) .collect(Collectors.toList())
と同じ意味。
html(Thymeleaf)で権限(Role)を取得する為の属性「sec:
」というものがある。(Spring Boot
1.5.6、Spring Security 4.2.3)
これを使う為には、依存ライブラリーにthymeleaf-extras-springsecurityを追加する必要がある。
〜
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4')
runtime('org.springframework.boot:spring-boot-devtools')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}
thymeleaf-extras-springsecurityにはSpring Securityのバージョン3用と4用があるらしいが、今回使っているのは4。
参考: masatsugumatsusさんのSpring Boot で Spring Security
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.springframework.org/schema/security"> <head> <meta charset="UTF-8" /> <title>User Thymeleaf</title> </head> <body> <h1>User Thymeleaf</h1> <p>user: <span sec:authentication="name">user name</span></p> <p>role: <span sec:authorize="hasRole('ROLE_ADMIN')">admin</span> <span sec:authorize="hasRole('ROLE_USER1')">user1</span> <span sec:authorize="hasRole('ROLE_USER2')">user2</span> </p> <hr /> <p><a href="index.html" th:href="@{/}">戻る</a></p> </body> </html>
「sec:authentication="name"
」を付けると、そのタグのボディー部はユーザーIDに置換される。
「sec:authorize="hasRole('ロール名')"
」を付けると、ユーザーがその権限を持っているときだけ、そのタグが有効になる。
xmlns:secは「http://www.springframework.org/schema/security」でも「http://www.thymeleaf.org/thymeleaf-extras-springsecurity4」でもいいようだけど、どちらを使うのがいいんだろう?
前者はディレクトリーが存在しているけど後者は404 Not Foundになるし…。
Spring Security 4.2.xのドキュメントには「http://www.springframework.org/security/tags」と書いてあるし、thymeleaf-extras-springsecurityのGitHubには「http://www.thymeleaf.org/extras/spring-security」と書いてあるし。
Controllerで権限(Role)を取得することも出来る。(Spring Boot 1.5.6、Spring Security 4.2.3)
package com.example.demo; import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.auth.ExampleRole;
@Controller public class UserController { @RequestMapping(path = "/user") public String user(Authentication authentication) { String userId = authentication.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); Set<ExampleRole> roles = authorities.stream() .map(GrantedAuthority::getAuthority).map(ExampleRole::valueOf) .collect(Collectors.toSet()); return "user"; } }
Controllerのメソッドの引数にAuthenticationを追加する。
ここからユーザーIDや権限を取得することが出来る。
ちなみに、Java8のStream APIを使ってGrantedAuthorityクラスから列挙型に変換している。
メソッド参照「::
」に慣れていない人向けにラムダ式で表すと、
Set<ExampleRole> roles = authorities.stream() .map(a -> ExampleRole.valueOf(a.getAuthority())) .collect(Collectors.toSet());
と同じ意味。
また、Authenticationから(LoginUserServiceで生成した)Userインスタンスを取得することも出来る。
import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User;
@RequestMapping(path = "/user") public String user2(Authentication authentication) { User user = (User) authentication.getPrincipal(); String userId = user.getUsername(); Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); return "user"; }
Userインスタンスは、(Authenticationを経由せずに)メソッドの引数で直接受け取ることも出来る。
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.User;
@RequestMapping(path = "/user") public String user3(@AuthenticationPrincipal User user) { String userId = user.getUsername(); Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); return "user"; }
参考: yokobonbonさんのSpring SecurityのhasRole(role)に要注意
WebSecurityConfigで、URLに関する権限を指定する事が出来る。[2017-10-31]
(特定の権限を持っていないとそのURLにアクセスできない)
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
〜
http.authorizeRequests().antMatchers("/maintenance/**").hasRole("ADMIN");
〜
}
Controller(Conrollerクラスまたはそのメソッド)に@Securedアノテーションを付けることで、URLに関する権限を指定する事が出来る。[2017-10-31]
(特定の権限を持っていないとそのURLにアクセスできない)
この場合、WebSecurityConfigで@EnableGlobalMethodSecurityアノテーションを付けておく必要がある。
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 〜 }
import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(path = "/maintenance")
@Secured("ROLE_ADMIN")
public class MaintenanceController {
〜
}
ユーザーに権限が無い場合、AccessDeniedExceptionが発生する。
参考: huruyosiさんのspring boot その8 - spring security で 認可を行う