課題
SpringSecurityのサンプルでは、以下のようにROLEに基づいて認可していることが多い。
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN")
が、ロールに基づいた認可ではなく権限に基づいた認可を行いたい。つまり、ロールごとに複数の権限を持たせ、ユーザにロールを複数割り当てたい。
参考 業務システムにおけるロールベースアクセス制御
解決方法
SpringSecurityでは、AuthenticationクラスをもとにアクセスコントロールするためのEL式が提供されている。
ログイン時にAuthenticationクラスのauthoritiesに権限情報を設定し、EL式を利用すれば権限に基づいた認可が実現できる。
参考 Spring Security 使い方メモ 認証・認可
参考 TERASOLUNA Server Framework for Java (5.x)
Expression Description hasRole([role]) Returns true if the current principal has the specified role. By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler. hasAuthority([authority]) Returns true if the current principal has the specified authority permitAll Always evaluates to true isAuthenticated() Returns true if the user is not anonymous ... ...
参考 27.1.1 Common Built-In Expressions
上記EL式の実装クラスは、SecurityExpressionRoot。 参考 SecurityExpressionRoot
hasRoleとhasAuthorityの違いは、権限の文字列に"ROLE_"プレフィックスをつけてくれるか、つけてくれないか。hasRoleのプレフィックスはROLE以外も指定できるため、同じふるまいにもできる。つまり、hasRoleという名前でおもいっきりロールを意識させられているが、実際にはロールじゃなくてもいい。ただのGrantedAuthorityの文字列のチェックでしかない。 が、名前がまぎらわしいので、権限に基づいた認可をする場合はhasAuthorityを利用したほうが無難だと思う。
実装例
ソースコード全体はgithubに上げました。
実装したアプリ。
- ロールは、『管理者』と『一般』の2つ
- 権限は、『権限管理』と『ユーザ一覧表示』の2つ
シナリオ
- 一般ユーザでログインし、『権限管理』を参照できないことを確認する
- 管理者ユーザでログインし、一般ユーザに『権限管理』の権限を与える
- 一般ユーザでログインしなおし、『権限管理』を参照できることを確認する
Userは複数のRoleを保持する。
@Data @Entity @NoArgsConstructor public class User implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; @Column(nullable = false) private String password; @ManyToMany private List<Role> roles; public User(String name, String password, List<Role> roles) { this.name = name; this.password = password; this.roles = roles; } }
Roleは複数のPermissionを保持する。
@Data @Entity @NoArgsConstructor public class Role implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; @ManyToMany private List<Permission> permissions; public Role(String name, List<Permission> permissions) { this.name = name; this.permissions = permissions; } public Role(Long id, List<Permission> permissions) { this.id = id; this.permissions = permissions; }
@Data @Entity @NoArgsConstructor public class Permission implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; public Permission(String name) { this.name = name; } public Permission(Long id) { this.id = id; } }
ユーザ情報の取得処理で、UserDetails
のGrantedAuthority
に権限を設定する。
CustomUserDetailsService.java
@Service @AllArgsConstructor public class CustomUserDetailsService implements UserDetailsService { UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User target = userRepository.findOneByName(username); List<SimpleGrantedAuthority> authorities = target.getRoles().stream() .flatMap(i -> i.getPermissions().stream()) .map(i -> new SimpleGrantedAuthority(i.getName())) .collect(Collectors.toList()); org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(target.getName(), target.getPassword(), authorities); return user; } }
あとは、権限に基づいて認可の設定をするだけ。
WebSecurityConfig.java
@EnableWebSecurity @AllArgsConstructor public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable() .authorizeRequests() .mvcMatchers(HttpMethod.PUT,"/api/roles/*").hasAuthority("CHANGE_ROLE") .mvcMatchers("/roles").hasAuthority("CHANGE_ROLE") .mvcMatchers("/users").hasAuthority("SHOW_ALL_USER") ... } ...