Spring Securityによるカスタムセキュリティ表現
1概要
このチュートリアルでは、Spring Securityを使ったカスタムセキュリティ式の作成に焦点を当てます。
時々、
フレームワークで利用可能な表現
は単純に表現が不十分です。そして、これらの場合、意味的に既存の表現より豊かな新しい表現を構築することは比較的簡単です。
最初にカスタム
PermissionEvaluator
を作成し、次に完全にカスタムの式を作成する方法について説明します。最後に、組み込みのセキュリティ式の1つをオーバーライドする方法について説明します。
2ユーザーエンティティ
まず、新しいセキュリティ表現を作成するための基盤を準備しましょう。
私たちの
User
エンティティを見てみましょう。
@Entity
public class User{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false, unique = true)
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users__privileges",
joinColumns =
@JoinColumn(name = "user__id", referencedColumnName = "id"),
inverseJoinColumns =
@JoinColumn(name = "privilege__id", referencedColumnName = "id"))
private Set<Privilege> privileges;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "organization__id", referencedColumnName = "id")
private Organization organization;
//standard getters and setters
}
そして、これが私たちのシンプルな
Privilege
です。
@Entity
public class Privilege {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false, unique = true)
private String name;
//standard getters and setters
}
そして私たちの
組織化
:
@Entity
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false, unique = true)
private String name;
//standard setters and getters
}
最後に、よりシンプルなカスタム
Principal
を使用します。
public class MyUserPrincipal implements UserDetails {
private User user;
public MyUserPrincipal(User user) {
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (Privilege privilege : user.getPrivileges()) {
authorities.add(new SimpleGrantedAuthority(privilege.getName()));
}
return authorities;
}
...
}
これらすべてのクラスの準備が整ったら、基本の
UserDetailsService
実装でカスタムの
Principal
を使用します。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new MyUserPrincipal(user);
}
}
ご覧のとおり、これらの関係について複雑なことは何もありません。ユーザーには1つ以上の特権があり、各ユーザーは1つの組織に属しています。
3データ設定
次に、簡単なテストデータでデータベースを初期化しましょう。
@Component
public class SetupData {
@Autowired
private UserRepository userRepository;
@Autowired
private PrivilegeRepository privilegeRepository;
@Autowired
private OrganizationRepository organizationRepository;
@PostConstruct
public void init() {
initPrivileges();
initOrganizations();
initUsers();
}
}
これが
__init
__メソッドです。
private void initPrivileges() {
Privilege privilege1 = new Privilege("FOO__READ__PRIVILEGE");
privilegeRepository.save(privilege1);
Privilege privilege2 = new Privilege("FOO__WRITE__PRIVILEGE");
privilegeRepository.save(privilege2);
}
private void initOrganizations() {
Organization org1 = new Organization("FirstOrg");
organizationRepository.save(org1);
Organization org2 = new Organization("SecondOrg");
organizationRepository.save(org2);
}
private void initUsers() {
Privilege privilege1 = privilegeRepository.findByName("FOO__READ__PRIVILEGE");
Privilege privilege2 = privilegeRepository.findByName("FOO__WRITE__PRIVILEGE");
User user1 = new User();
user1.setUsername("john");
user1.setPassword("123");
user1.setPrivileges(new HashSet<Privilege>(Arrays.asList(privilege1)));
user1.setOrganization(organizationRepository.findByName("FirstOrg"));
userRepository.save(user1);
User user2 = new User();
user2.setUsername("tom");
user2.setPassword("111");
user2.setPrivileges(new HashSet<Privilege>(Arrays.asList(privilege1, privilege2)));
user2.setOrganization(organizationRepository.findByName("SecondOrg"));
userRepository.save(user2);
}
ご了承ください:
-
ユーザー “john”は
FOO
READ
PRIVILEGE
のみを持っています -
ユーザ “tom”は
FOO
READ
PRIVILEGE
と
FOO
WRITE
PRIVILEGE
の両方を持っています
4カスタム許可エバリュエーター
この時点で、新しいカスタム許可エバリュエータを介して、新しい表現を実装する準備が整いました。
ユーザーの権限を使用してメソッドを保護します。ただし、ハードコードされた権限名を使用する代わりに、よりオープンで柔軟な実装を実現したいと思います。
始めましょう。
4.1.
PermissionEvaluator
独自のカスタムパーミッションエバリュエータを作成するには、
PermissionEvaluator
インタフェースを実装する必要があります。
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(
Authentication auth, Object targetDomainObject, Object permission) {
if ((auth == null) || (targetDomainObject == null) || !(permission instanceof String)){
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
return hasPrivilege(auth, targetType, permission.toString().toUpperCase());
}
@Override
public boolean hasPermission(
Authentication auth, Serializable targetId, String targetType, Object permission) {
if ((auth == null) || (targetType == null) || !(permission instanceof String)) {
return false;
}
return hasPrivilege(auth, targetType.toUpperCase(),
permission.toString().toUpperCase());
}
}
これが
hasPrivilege()
メソッドです。
private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
if (grantedAuth.getAuthority().startsWith(targetType)) {
if (grantedAuth.getAuthority().contains(permission)) {
return true;
}
}
}
return false;
}
-
新しいセキュリティ表現が利用可能になり、使用する準備が整いました。
hasPermission
。**
そして、もっとハードコードされたバージョンを使う代わりに、
@PostAuthorize("hasAuthority('FOO__READ__PRIVILEGE')")
使用できます:
@PostAuthorize("hasPermission(returnObject, 'read')")
または
@PreAuthorize("hasPermission(#id, 'Foo', 'read')")
注:
#id
はメソッドパラメータを表し、 ‘
Foo
‘はターゲットオブジェクトタイプを表します。
4.2. メソッドセキュリティ設定
CustomPermissionEvaluator
を定義するだけでは十分ではありません。メソッドのセキュリティ設定でも使用する必要があります。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}
** 4.3. 実際の例
**
では、いくつかの簡単なコントローラメソッドで、新しい式を使い始めましょう。
@Controller
public class MainController {
@PostAuthorize("hasPermission(returnObject, 'read')")
@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) {
return new Foo("Sample");
}
@PreAuthorize("hasPermission(#foo, 'write')")
@RequestMapping(method = RequestMethod.POST, value = "/foos")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public Foo create(@RequestBody Foo foo) {
return foo;
}
}
そしてそこに行きます – 私たちはすべて新しい式を実際に設定して使用しています。
+
4.4. ライブテスト
それでは、簡単なライブテストを書いてみましょう。APIにアクセスして、すべてが正常に機能することを確認します。
@Test
public void givenUserWithReadPrivilegeAndHasPermission__whenGetFooById__thenOK() {
Response response = givenAuth("john", "123").get("http://localhost:8081/foos/1");
assertEquals(200, response.getStatusCode());
assertTrue(response.asString().contains("id"));
}
@Test
public void givenUserWithNoWritePrivilegeAndHasPermission__whenPostFoo__thenForbidden() {
Response response = givenAuth("john", "123").contentType(MediaType.APPLICATION__JSON__VALUE)
.body(new Foo("sample"))
.post("http://localhost:8081/foos");
assertEquals(403, response.getStatusCode());
}
@Test
public void givenUserWithWritePrivilegeAndHasPermission__whenPostFoo__thenOk() {
Response response = givenAuth("tom", "111").contentType(MediaType.APPLICATION__JSON__VALUE)
.body(new Foo("sample"))
.post("http://localhost:8081/foos");
assertEquals(201, response.getStatusCode());
assertTrue(response.asString().contains("id"));
}
そして、これが私たちの
givenAuth()
メソッドです:
private RequestSpecification givenAuth(String username, String password) {
FormAuthConfig formAuthConfig =
new FormAuthConfig("http://localhost:8081/login", "username", "password");
return RestAssured.given().auth().form(username, password, formAuthConfig);
}
5新しいセキュリティ表現
前のソリューションでは、
hasPermission
式を定義して使用することができました – これは非常に便利です。
ただし、ここでは式自体の名前と意味によって、まだ多少制限があります。
そのため、このセクションでは、フルカスタム化し、
isMember()
というセキュリティ式を実装して、プリンシパルが組織のメンバーであるかどうかを確認します。
+
5.1. カスタムメソッドセキュリティ式
この新しいカスタム式を作成するには、まずすべてのセキュリティ式の評価が始まるルートノートを実装する必要があります。
public class CustomMethodSecurityExpressionRoot
extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
public CustomMethodSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public boolean isMember(Long OrganizationId) {
User user = ((MyUserPrincipal) this.getPrincipal()).getUser();
return user.getOrganization().getId().longValue() == OrganizationId.longValue();
}
...
}
これで、この新しい操作をどのようにしてルートノートに追加したのかがわかります。
isMember()
は、現在のユーザーが特定の
Organization
のメンバーであるかどうかを確認するために使用されます。
組み込み式も含めるために
SecurityExpressionRoot
を拡張した方法にも注意してください。
5.2. カスタム式ハンドラ
次に、式ハンドラに
CustomMethodSecurityExpressionRoot
を挿入する必要があります。
public class CustomMethodSecurityExpressionHandler
extends DefaultMethodSecurityExpressionHandler {
private AuthenticationTrustResolver trustResolver =
new AuthenticationTrustResolverImpl();
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root =
new CustomMethodSecurityExpressionRoot(authentication);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(this.trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}
5.3. メソッドセキュリティ設定
それでは、メソッドセキュリティ設定で
CustomMethodSecurityExpressionHandler
を使用する必要があります。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
CustomMethodSecurityExpressionHandler expressionHandler =
new CustomMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}
5.4. 新しい式を使う
これは、
isMember()
を使用してコントローラメソッドを保護する簡単な例です。
@PreAuthorize("isMember(#id)")
@RequestMapping(method = RequestMethod.GET, value = "/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
return organizationRepository.findOne(id);
}
5.5. ライブテスト
最後に、これはユーザ“
john
”のための簡単なライブテストです。
@Test
public void givenUserMemberInOrganization__whenGetOrganization__thenOK() {
Response response = givenAuth("john", "123").get("http://localhost:8081/organizations/1");
assertEquals(200, response.getStatusCode());
assertTrue(response.asString().contains("id"));
}
@Test
public void givenUserMemberNotInOrganization__whenGetOrganization__thenForbidden() {
Response response = givenAuth("john", "123").get("http://localhost:8081/organizations/2");
assertEquals(403, response.getStatusCode());
}
6. 組み込みセキュリティ式を無効にする
最後に、組み込みのセキュリティ表現を上書きする方法を見てみましょう –
hasAuthority()
を無効にすることについて議論しましょう。
6.1. カスタムセキュリティ表現ルート
私たち自身の
SecurityExpressionRoot
を書くことによって同様に始めましょう – 主として組み込みのメソッドが
final
であり、そして我々がそれらをオーバーライドすることができないので:
public class MySecurityExpressionRoot implements MethodSecurityExpressionOperations {
public MySecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
}
this.authentication = authentication;
}
@Override
public final boolean hasAuthority(String authority) {
throw new RuntimeException("method hasAuthority() not allowed");
}
...
}
このルートノートを定義したら、それを式ハンドラにインジェクトしてから、そのハンドラを構成に配線する必要があります。これについては、セクション5で説明したのと同じです。
** 6.2. 例題 – 式を使う
**
ここで、メソッドを保護するために
hasAuthority()
を使用したい場合 – 次のように、メソッドにアクセスしようとすると
RuntimeException
がスローされます。
@PreAuthorize("hasAuthority('FOO__READ__PRIVILEGE')")
@RequestMapping(method = RequestMethod.GET, value = "/foos")
@ResponseBody
public Foo findFooByName(@RequestParam String name) {
return new Foo(name);
}
6.3. ライブテスト
最後に、これが簡単なテストです。
@Test
public void givenDisabledSecurityExpression__whenGetFooByName__thenError() {
Response response = givenAuth("john", "123").get("http://localhost:8081/foos?name=sample");
assertEquals(500, response.getStatusCode());
assertTrue(response.asString().contains("method hasAuthority() not allowed"));
}
7. 結論
このガイドでは、既存のセキュリティ式では不十分な場合に、Spring Securityでカスタムセキュリティ式を実装できるさまざまな方法について詳しく説明しました。
そして、いつものように、完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/spring-security-mvc-boot[over GitHub]にあります。