參考資料 How to configure Spring Security Authorization - Java Brains
參考資料 Five Spring Security Concepts - Authentication vs authorization - Java Brains Brain Bytes
參考資料 Spring Security 什么是Principal
參考資料 Spring Security 入門之基本概念
注意,這篇筆記是接著上一篇 SpringSecurity 編寫一個簡單認證Demo 筆記的項目接著拓展的
Authorization 的一些概念

Principal
身份(Principal),即主體的標識屬性,可以是任何東西,如用戶名、郵箱等,唯一即可。一個主體可以有多個principal,但只有一個 Primary principal,一般是用戶名/密碼/手機號。
Principle 與 User 的差異是,Principal 是指一個可辨識的唯一身份,用來代表一個人,一個公司,一個裝置或另一個系統,并不限定于代表一個人,而用戶(User)是指與系統互動的操作者(人)。
Granted Authority
在 Spring Security 中,可以將每個 GrantedAuthority 視為一個單獨的特權。比如 READ_AUTHORITY,WRITE_PRIVILEGE 甚至 CAN_EXECUTE_AS_ROOT。
當直接使用 GrantedAuthority 時,例如通過使用諸如 hasAuthority('READ_AUTHORITY') 之類的表達式,將以細粒度的方式限制訪問。
@Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/protectedbyauthority").hasAuthority("READ_PRIVILEGE")
}
Roles
在 Spring Security 中,可以將每個 Role 視為一個粗粒度的 GrantedAuthority ,此種 GrantedAuthority 以 ROLE 為前綴的字符串表示。當直接使用 Role 時,可使用 hasRole("ADMIN") 之類的表達式,將以粗粒度方式限制訪問。值得注意的是,默認的 “ROLE” 前綴是可以配置的。
@Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/protectedbyrole").hasRole("USER")
}
權限的粒度

注意這個 GrantedAuthority 和 Roles 它們的粒度不同,Roles 一般代表更粗粒度的權限
Authority、Role 之間使用區別
參考資料 spring security中Authority、Role的區別
在編寫權限控制時發現有兩個種 API,一個是基于 Authority 判斷,一個是基于 Role 判斷

看注釋就能理解,如果你使用的是hasRole方法來判斷你的登錄用戶是否有權限訪問某個接口,那么你初始化User時,放入的 GrantedAuthority 的字符就需要包含 ROLE_ 前綴,參見下圖紅箭頭:

而接口的訪問卻不用加這個 ROLE_ 前綴

反之,如果使用的是 hasAuthority 方法則無需在 GrantedAuthority 的字符加上 ROLE_ 前綴
為什么要區分 Authority、Role
參考資料 Granted Authority Versus Role in Spring Security
一般 Role 是粗粒度的 Authority,如下所示,使用 Authority 可以標識它是個只讀權限
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
.antMatchers("/protectedbyrole").hasRole("USER")
.antMatchers("/protectedbyauthority").hasAuthority("READ_PRIVILEGE")
// ...
}
而且,對于自定義的數據存儲時(SQL),可以讓一個 Role 內部包含多個 GrantedAuthority,如下所示
// 這個 Role 類是自定義的,它里面存了一些 GrantedAuthority
private Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role: roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
role.getPrivileges().stream()
.map(p -> new SimpleGrantedAuthority(p.getName()))
.forEach(authorities::add);
}
return authorities;
}
修改下 Controller
對上個示例的 Controller 進行修改,將其分為
- ResourceController
- AuthController

@Slf4j
@RestController
public class ResourceController {
@GetMapping("/hello")
public String hello() {
return "<h1>Welcome</h1>";
}
@GetMapping("/user")
public String user() {
return "<h1>Welcome User</h1>";
}
@GetMapping("/admin")
public String admin() {
return "<h1>Welcome Admin</h1>";
}
}
修改下 MyDetailsService
之前這個 MyDetailsService 里面就存一個用戶,現在需要添加多個角色,所以相應的用戶也應該存多個,因為是測試,所以這里使用 Map 來存這些測試用戶
@Service
public class MyDetailsService implements UserDetailsService {
static Map<String, User> users;
static {
users = new HashMap<>();
// 注意:使用 SimpleGrantedAuthority 需要加上 ROLE_ 否則無法比對
users.put("foo",new User("foo", "foopassword",
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))));
users.put("admin", new User("admin","admin", Stream.of(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_ADMIN")
).collect(Collectors.toList())));
}
/**
* 這個 UserDetailsService 一般只用于到 DAO 層加載用戶數據
*/
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
return users.get(name);
}
}
配置 HttpSecurity
對之前的那個 config 進行一下修改
對上面的各個 API 添加配置一下規則
@Override
public void configure(HttpSecurity http) throws Exception {
// 先關閉 CSRF 防護(跨站請求偽造,其實就是使用 Cookie 的那堆屁事,如果使用 JWT 可以直接關閉它)
http.csrf().disable()
.authorizeRequests()
// 這個 antMatcher 方法用于匹配請求(注意方法名后面要加 's')
.antMatchers(HttpMethod.POST, "/authenticate").permitAll()
.antMatchers("/hello").permitAll() // 任意都可以訪問
.antMatchers("/user").hasAnyRole("USER","ADMIN")
.antMatchers("/admin").hasAnyRole("ADMIN")
.anyRequest().authenticated()
// 這里關閉 Session 驗證(就是 Cookie-Session 那個)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 把自己注冊的過濾器放在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
上面的那種方式也可以使用注解的方式
首先在啟動類上加 @EnableGlobalMethodSecurity 注解開啟 Security 注解支持
因為默認 @EnableGlobalMethodSecurity 的注解都是單獨設置的且全部為 false,所以需要手動開啟
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
然后就可以直接使用注解了(詳情看訪問控制那篇筆記)
@Slf4j
@RestController
public class ResourceController {
@GetMapping("/hello")
public String hello() {
return "<h1>Welcome</h1>";
}
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public String user() {
return "<h1>Welcome User</h1>";
}
@GetMapping("/admin")
@PreAuthorize("hasAnyRole('ADMIN')")
public String admin() {
return "<h1>Welcome Admin</h1>";
}
}
使用 Mock 測試
這里基本和之前那個沒區別,只是請求地址換個試試
@Test
void hello() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.get("/user")
.header("Authorization", "Bearer " + jwt) // 別忘了要加個空格
)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
;
}
接下來可以使用不同的用戶登陸訪問各個授權 API 試試
浙公網安備 33010602011771號