S-JIS[2017-08-28/2017-10-31] 変更履歴

Spring Boot Role

Spring BootのウェブアプリケーションのRole(権限・役割)のメモ。


概要

Spring Securityにはユーザー毎の権限(Role)を管理する機能がある。
これにより、特定のユーザーしか表示できない画面や処理を実現することが出来る。

Spring Bootで用意されているUserDetailsは権限を(保持・)取得できるようになっており、Controllerhtml(Thymeleaf)で取得することが出来る。


UserDetailsの例

UserDetailsは、Spring Securityでユーザー情報を管理するクラス(インターフェース)。(Spring Boot 1.5.6、Spring Security 4.2.3)

UserDetailsServiceでUserDetailsのインスタンスを返すよう実装する。
ここで、UserDetailsに権限(Role)をセットする。


Spring Securityでは権限(Role)は文字列で扱うが、普通は権限の種類は決まっているはずなので、列挙型にするのが自然だろう。

src/main/java/com/example/demo/auth/ExampleRole.java:

package com.example.demo.auth;

public enum ExampleRole {

	ROLE_USER1, ROLE_USER2, ROLE_ADMIN, ROLE_API
}

権限名は「ROLE_」で始まるよう名付けておく。
(文字列として扱うときに「ROLE_」が付いていればいいが、列挙子名が違うのもバグの元になると思われるので、列挙子名にも「ROLE_」を付けてある)

参考: yokobonbonさんのSpring SecurityのhasRole(role)に要注意


src/main/java/com/example/demo/auth/LoginUserService.java:

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)で権限を取得する例

html(Thymeleaf)で権限(Role)を取得する為の属性「sec:」というものがある。(Spring Boot 1.5.6、Spring Security 4.2.3)

これを使う為には、依存ライブラリーにthymeleaf-extras-springsecurityを追加する必要がある。

build.gradle:

〜
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


src/main/resources/templates/user.html:

<!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で権限を取得する例

Controllerで権限(Role)を取得することも出来る。(Spring Boot 1.5.6、Spring Security 4.2.3)

src/main/java/com/example/demo/UserController.java:

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)に要注意


URL権限チェック

WebSecurityConfigのURL権限チェック

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のURL権限チェック

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


Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま