把玩 Spring Security [2] 探索 Access Control 功能
- notes spring-security
在先前的實驗中,我們製作了一個新的 Security Filter 放行了任何 HTTP REQUEST。在這個基礎上,我們可以來探索 Spring Security 的 Access Control。
在安全的領域中,有二個主要的術語要認得:
- Authentication
- Authorization
在前一篇介紹,我們知道了 Spring Security 如何得知一個 HTTP REQUEST 有沒有獲得 Authentication,即為 獲得身份驗證
。系統知道這一個 HTTP REQUST 是代表者某一個特定的使用者,或特定的裝置,或更通俗一點的 Client。那麼在獲得身份之後,一個安全系統會關心的問題是,用這個身份能做些什麼?即為 Authorization,被 授權
了哪些能力。
路徑與授權控制
將上回的範例修改如下,在 antMatchers
後,我們多加了 hasRole
去限制對應路徑需要的 授權
:
@Component
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new FriendlyFilter(), LogoutFilter.class);
http.csrf().disable()
.authorizeRequests()
.antMatchers("/users/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/info")
.authenticated()
.anyRequest().permitAll();
}
}
這樣的寫法相當直覺好懂:
- 修改
/users
指令要有 USER Role 的人才能使用 - 增加
/admin
並指定要有 ADMIN Role 的人才能使用 - 增加
/info
沒有特定角色的授權,只要登入就行了 - 沒有提到的
/
是由anyRequest()
開通無限制存取permitAll()
由於增加了這部分,我們的 Controller 也會加一些新的方法:
@RestController
public class SimpleController {
@RequestMapping("/")
public String home() {
return "home";
}
@RequestMapping("/users")
public String users() {
return "users";
}
@RequestMapping("/admin")
public String admin() {
return "admin";
}
@RequestMapping("/info")
public String info() {
return "info";
}
}
提供授權資訊
在目前版本的 Security Filter 除了回應 isAuthenticated()
之外沒有額外提供進一步資訊:
public class FriendlyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(new ApiToken());
filterChain.doFilter(request, response);
}
}
我們能透過它來提供授權資料,ApiToken
只是單純實作了 Authentication
介面,並強制 isAuthenticated
回應 true
罷了。這麼作只是為了讓 Spring Security 認為這個 HTTP REQUEST 是已通過驗證。授權資料可以透過覆寫 getAuthorities()
方法來提供:
SecurityContextHolder.getContext().setAuthentication(new ApiToken() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(
new SimpleGrantedAuthority("ROLE_ADMIN"));
}
});
這簡單地修改,強制了目前的 Authentication 物件,回傳了一個含有 ROLE_ADMIN
的授權資料。這邊比先前的 hasRole
中的設定,多出了 ROLE_
前綴字串,這是預設的 AccessDecisionVoter
的行為 (RoleVoter
)。簡單實測,僅有需要 USER role 的 /users
被阻擋:
$ curl http://127.0.0.1:8787/admin
admin
$ curl http://127.0.0.1:8787/users
{"timestamp":"2021-09-27T12:50:50.232+00:00","status":403,"error":"Forbidden","path":"/users"}
$ curl http://127.0.0.1:8787/info
info
使用標註式授權
除了透過 HttpSecurity 對特定路徑指令可以存取的角色,也能透過 Annotation 來指定授權。這樣,權限就能針對任何 Spring 管理的物件方法進行設定,以下是使用 @PreAuthorize
指定 info 方法需要有 USER role 才能使用:
@PreAuthorize("hasRole('USER')")
@RequestMapping("/info")
public String info() {
return "info";
}
但別忘了,這種用法需要對 Spring Boot Application 加開新的設定 @EnableGlobalMethodSecurity
:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@SpringBootApplication
public class LearningSpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(LearningSpringSecurityApplication.class, args);
}
}
- prePostEnabled 是啟用 @PreAuthorize / @PostAuthorize
- securedEnabled 是啟用 @Secured
重點摘要
- 學習在 HttpSecurity 中,利用
hasRole()
指定路徑需要的角色權限 - Security Filter 產生的 Authentication 物件,可以透過
getAuthorities()
方法提供授權資料。 - 提用 Annotation 的方式標註權限控管
到目前為止,你可能會發現我們都還沒有談到循序圖中的 AuthenticationManager
。先不談它是可以降低學習的門檻,因為身份驗證的方式百百種,看了太多的方式難勉眼花撩亂而壞了學習的興致。我們可以簡略地想成這樣:
public class FriendlyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 運用 authenticationManager 完成身份驗證
Authentication authentication = authenticationManager.authenticate(...);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
這段程式範例,它與先前直接 new ApiToken()
沒有差太多,只是改成了可變的,是由查詢而來的!而要去哪查詢就依不同的 AuthenticationProvider 實作來決定了,可以是 OAuth Provider 或是 JDBC 連線查詢,當然也可以是 JWT 解碼後的資料。有了這樣的認知後 Authentication 就不再是難以掌握的主題。
本篇完整範例請參考: