spring security 授權方式(自定義)及源碼跟蹤
這節我們來看看spring security
的幾種授權方式,及簡要的源碼跟蹤。在初步接觸spring security時,為了實現它的授權,特別是它的自定義授權,在網上找了特別多的文章以及例子,覺得好難,但是現在自己嘗試結合官方文檔及demo來學習,頗有收獲。
基於表達式Spel的訪問控制
Spring Security 使用 Spring EL 進行表達式支持,不了解Spring EL的童鞋自行學習。根據文檔https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#el-access 在IDEA中查看SecurityExpressionRoot
類的上下繼承關系,SecurityExpressionOperations聲明了各個表達式接口,最終由WebSecurityExpressionRoot、MethodSecurityExpressionRoot實現各個具體的表達式邏輯。繼承關系如下所示:
在這里我們能夠知道,最常用的應該就是基於Web\Method這兩種方式來進行授權我們的應用。再來看下 SecurityExpressionRoot 類中定義的最基本的 SpEL 有哪些:
我們簡單介紹幾個表達式接口:
Expression | Description |
---|---|
hasRole(String role) | 如果當前主體具有指定的角色,則返回 true |
hasAnyRole(String… roles) | 如果當前主體具有任何提供的角色,則返回 true |
hasAuthority(String authority) | 如果當前主體具有指定的權限,則返回 true。 |
hasAnyAuthority(String… authorities) | 如果當前主體具有任何提供的權限,則返回 true |
authentication | 允許直接訪問從 SecurityContext 獲得的當前 Authentication 對象 |
principal | 允許直接訪問代表當前用戶的主體對象 |
授權方式
基於Web/Url的安全表達式
這種方式可以對單個Url進行安全驗證,也可以對批量的Url進行安全驗證,比如
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 針對/admin/api/hello 這個接口要有p1權限
.antMatchers("/admin/api/hello").hasAuthority("p1")
// 對於訪問/user/api/** 的接口要有user權限
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login-error")
.permitAll();
}
基於Method的安全表達式
Method Security Expressions
Method security is a bit more complicated than a simple allow or deny rule. Spring Security 3.0 introduced some new annotations in order to allow comprehensive support for the use of expressions.
Spring Security 3.0 引入了一些新的注解,以便全面支持表達式的使用,分別是@PreAuthorize
, @PreFilter
, @PostAuthorize
and @PostFilter
相信大家有web開發的基礎,不難知道這幾個注解的意思。
-
@PreAuthorize:在訪問方法前進行鑒權
-
@PreFilter:同上
-
@PostAuthorize:在訪問方法后進行鑒權
-
@PostFiltert:同上
public class AdminController { @GetMapping("hello") @PostAuthorize("hasRole('User')") public String hello() { return "hello, admin"; } @GetMapping("p1") @PreAuthorize("hasAuthority('p1')") public String p1() { return "hello, p1"; } }
但是基於方法的需要事先在配置類添加注解,表示開啟方法驗證。
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
根據官網介紹還有其他2中方式(基於AOP 、spring security原生的注釋@Secure),有興趣的小伙伴可以自行參閱https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls
授權原理
根據文檔https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls指出,spring security提供了攔截器來控制安全對象的訪問,例如方法調用、web請求。AccessDecisionManager 做出關於是否允許調用繼續進行的調用前決定。
查看AccessDecisionManager 接口:
decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
參數說明:
- authentication:當前登錄對象主體
- object: 當前安全保護對象
- configAttributes:訪問當前對象必須要有的權限屬性
再看看看它的三個實現類,默認的實現是根據各個實現類的投票機制來決定是否能夠訪問當前安全保護對象的:
AffirmativeBased:只要configAttributes中有一個權限滿足就可以訪問當前保護對象
ConsensusBased:滿足超過一半的權限就能夠訪問當前保護對象
UnanimousBased:configAttributes中所有的權限都滿足才能訪問當前保護對象
由於我們不知道默認是哪個實現類,所以我們在三個類上的decide方法都打上斷點,這樣我們就能知道默認是哪個實現類了,
內部的投票實現有興趣的小伙伴自行探索,到這樣我們大概就明白spring security默認的授權實現機制了。接着我們根據該機制去實現我們的自定義授權方式。
給出官網的一張原理圖
- 首先,FilterSecurityInterceptor 從 SecurityContextHolder 獲得一個 Authentication
- 其次,FilterSecurityInterceptor 從傳入 FilterSecurityInterceptor 的 HttpServletRequest、HttpServletResponse 和 FilterChain 創建一個 FilterInvocation
- 接下來,它將 FilterInvocation 傳遞給 SecurityMetadataSource 以獲取 ConfigAttributes
- 最后,它將 Authentication、FilterInvocation 和 ConfigAttributes 傳遞給 AccessDecisionManager。
- 如果授權被拒絕,則拋出 AccessDeniedException。在這種情況下,ExceptionTranslationFilter 處理 AccessDeniedException
- 如果訪問被授予,FilterSecurityInterceptor 繼續使用允許應用程序正常處理的 FilterChain。
自定義授權方式
根據葫蘆畫瓢,我們首先需要
1、自定義一個AccessDecisionManager實現類,讓它確定到底是否能夠鑒權通過,能夠訪問保護對象;
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登錄,請登錄!");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("權限不足,請聯系管理員!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
2、接着實現一個FilterInvocationSecurityMetadataSource實現類,這個類給出訪問保護對象具體需要的哪些權限。
/**
* 這個類的作用,主要是根據用戶傳來的請求地址,分析出請求需要的角色
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
3、將上面2個對象添加到攔截器中,給FilterSecurityInterceptor重新設置它的這2個屬性
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
相信到這里,小伙伴也能根據自己實際項目需要怎樣的授權方式去進行實現了,如果是AOP/@secure方式的則需要再看一下文檔說明。好了,spring security的章節就到這里,后面繼續學習spring security oauth2的章節。