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 で 認可を行う