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;
    }

    ...
}

これらすべてのクラスの準備が整ったら、基本の UserDetailsS​​ervice 実装でカスタムの 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]にあります。

前の投稿:AutoValueの紹介
次の投稿:Javaでリスト実装をTDDする方法