只要和用戶打交道的系統(tǒng)基本都需要進(jìn)行權(quán)限管理店展,不然哪一天操作不當(dāng)給你刪庫了怎么辦伐谈。開源的權(quán)限管理框架有SpringSecurity、Shiro民褂,權(quán)限管理模型有RBAC茄菊、ACL等,是選擇開源框架好還是基于權(quán)限管理模型造輪子好赊堪,必須都調(diào)研一下選一個(gè)適合公司業(yè)務(wù)的實(shí)現(xiàn)方式面殖,首先先調(diào)研學(xué)習(xí)一波SpringSecurity
通過本篇文章你將了解到
-
SpringSecurity
中的一些核心類 - 使用
SpringSecurity
基于角色的權(quán)限校驗(yàn) -
SpringSecurity
的不足
SpringSecurity核心類
因?yàn)閷?code>SpringSecurity只是調(diào)研性的學(xué)習(xí),所以這里并不會對源碼進(jìn)行介紹哭廉。權(quán)限管理就是授權(quán)脊僚、鑒權(quán),要想鑒權(quán)必須首先登陸拿到用戶信息遵绰,但是這里也不會去講登陸辽幌、登出以及分布式session
管理,只會介紹一下鑒權(quán)過程中的核心類椿访,了解一下核心類可以快速整合SpringSecurity
框架乌企。
登陸校驗(yàn)
-
UsernamePasswordAuthenticationFilter
=> 用戶登陸驗(yàn)證,但這個(gè)類里并不會正真去進(jìn)行登陸校驗(yàn)赎离,而是通過ProviderManager -
ProviderManager
=> 這個(gè)類里有一個(gè)List<AuthenticationProvider>
逛犹,提供了不同的校驗(yàn)方式,只要其中一個(gè)通過即可梁剔。一般我們什么都不配的情況下是根據(jù)用戶名和密碼虽画,這時(shí)候AuthenticationProvider
實(shí)現(xiàn)類為AbstractUserDetailsAuthenticationProvider
-
AbstractUserDetailsAuthenticationProvider
=> 其登陸校驗(yàn)是通過子類的additionalAuthenticationChecks
方法完成的 -
DaoAuthenticationProvider
=>AbstractUserDetailsAuthenticationProvider
的唯一子類,如果我們設(shè)定的密碼(可以基于內(nèi)存也可以基于數(shù)據(jù)庫)和傳過來的密碼比對不上登陸校驗(yàn)失敗 -
UserDetailsService
=> 通過這個(gè)接口唯一的方法loadUserByUsername
返回我們設(shè)定的用戶名和密碼等用戶信息(被封裝為UserDetails
的實(shí)現(xiàn)類)
小結(jié):登陸驗(yàn)證就是拿到客戶端的用戶名密碼和我們設(shè)定的用戶名密碼(即UserDetails
的實(shí)現(xiàn)類)進(jìn)行比對荣病,拿到我們設(shè)定的用戶名密碼是通過UserDetailsService.loadUserByUsername(String userName)
實(shí)現(xiàn)的[劃重點(diǎn)码撰,一會要考],框架實(shí)現(xiàn)的UserDetailsService
一般沒法滿足項(xiàng)目要求个盆,就需要自己手動實(shí)現(xiàn)了脖岛,同時(shí)如果框架自帶的UserDetails
的實(shí)現(xiàn)類沒法滿足要求我們也可以自己實(shí)現(xiàn)UserDetails
權(quán)限校驗(yàn)
-
FilterSecurityInterceptor
=> 基于角色的權(quán)限校驗(yàn)攔截器,調(diào)用父類AbstractSecurityInterceptor
的beforeInvocation
方法進(jìn)行鑒權(quán) -
AbstractSecurityInterceptor
=> 調(diào)用AccessDecisionManager
實(shí)現(xiàn)類的decide
方法進(jìn)行鑒權(quán)颊亮,所以如何想自定義鑒權(quán)方式可以寫一個(gè)類然后實(shí)現(xiàn)AccessDecisionManager
-
AffirmativeBased
=> 默認(rèn)使用的AccessDecisionManager
實(shí)現(xiàn)類柴梆,調(diào)用AccessDecisionVoter
實(shí)現(xiàn)類的vote方法進(jìn)行鑒權(quán),返回1權(quán)限校驗(yàn)通過终惑,其實(shí)跟蹤到最后其實(shí)還是比對字符串不能說是投票吧绍在,方法名容易讓人誤解 -
WebExpressionVoter
=> 默認(rèn)使用的AccessDecisionVoter
實(shí)現(xiàn)類,調(diào)用Authentication的authentication
方法進(jìn)行鑒權(quán) -
SecurityExpressionOperations
=> 獲取Authentication
對象接口 -
SecurityExpressionRoot
=>SecurityExpressionOperations
實(shí)現(xiàn)類
小結(jié):一般不自定鑒權(quán)方式的話這些我們都可以不需要管,雖然層層調(diào)用了很多層偿渡,其實(shí)實(shí)質(zhì)就是判斷當(dāng)前的用戶所包含的權(quán)限列表中是否包含訪問指定url所需要的權(quán)限
SpringBoot整合SpringSecurity
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>1.4.1.RELEASE</version>
</dependency>
UserDetails實(shí)現(xiàn)類:UserDTO.java
public class UserDTO implements UserDetails {
/**
* 用戶名
* */
private String username;
/**
* 密碼
* */
private String password;
/**
* 角色列表
* */
private List<String> roleList;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public List<String> getRoleList() {
return roleList;
}
public void setRoleList(List<String> roleList) {
this.roleList = roleList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorityList = new ArrayList<>();
for (String role : roleList) {
authorityList.add(new SimpleGrantedAuthority(role));
}
return authorityList;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
實(shí)現(xiàn)類需要實(shí)現(xiàn)接口臼寄,這里我們將查詢到的roleList
字符串封裝到SimpleGrantedAuthority
中,SimpleGrantedAuthority
是GrantedAuthority
的一個(gè)實(shí)現(xiàn)類溜宽,如果默認(rèn)實(shí)現(xiàn)沒法滿足需求可自己重新實(shí)現(xiàn)吉拳。UserDetail
是SpringSecurity
和應(yīng)用之間的橋梁,不管你數(shù)據(jù)庫怎么建适揉,只要你最后將用戶信息和權(quán)限的關(guān)系封裝為UserDetails
留攒,SpringSecurity
就可以按它自己的機(jī)制進(jìn)行權(quán)限校驗(yàn)
UserDetailsService實(shí)現(xiàn)類:UserDetailsServiceImpl.java
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UsersService usersService;
/**
* 根據(jù)用戶名獲取用戶信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = new Users();
users.setUsername(username);
List<Users> usersList = usersService.selectList(users);
return buildUserDTO(usersList);
}
/**
* 封裝UserDTO對象
*
* @param usersList
* @return
* */
private UserDTO buildUserDTO(List<Users> usersList) {
UserDTO userDTO = new UserDTO();
userDTO.setUsername(usersList.get(0).getUsername());
userDTO.setPassword(usersList.get(0).getPassword());
List<String> roleList = new ArrayList<>();
for (Users users : usersList) {
roleList.add(String.format("ROLE_%s", users.getRole()));
}
userDTO.setRoleList(roleList);
return userDTO;
}
}
該類作用就是將用戶信息和權(quán)限信息從數(shù)據(jù)庫查找到封裝為一個(gè)UserDetails
返回
權(quán)限配置類:WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啟方法級安全驗(yàn)證
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何請求,登錄后可以訪問
.and().formLogin().permitAll(); //登錄頁面用戶任意訪問
// 關(guān)閉CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 設(shè)置攔截忽略文件夾,可以對靜態(tài)資源放行
web.ignoring().antMatchers("/css/**", "/js/**", "/templates/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//1.設(shè)置自定義userDetailService
//2.校驗(yàn)時(shí)指定密碼解碼方式
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
這個(gè)類里配置了登陸頁面以及一些簡單權(quán)限設(shè)置涡扼,比如任何請求登陸后可訪問稼跳、登錄頁面所有人可以訪問盟庞。因?yàn)橥ㄟ^@EnableGlobalMethodSecurity
注解開啟了方法級別驗(yàn)證吃沪,在這個(gè)方法里沒有配置方法級別的權(quán)限。同時(shí)通過指定自己實(shí)現(xiàn)的UserDetailsService
以及密碼解碼方式什猖,如果不指定密碼解碼方式將會報(bào)錯(cuò)在最新的SpringSecurity
版本中
控制層注解使用方式
@RestController
@RequestMapping("/api/user")
public class UsersController {
@GetMapping("/guest")
@PreAuthorize("hasAnyRole('guest')")
public Object guest() {
return "hello guest";
}
@PreAuthorize("hasAnyRole('admin')")
@GetMapping("/admin")
public Object admin() {
return "hello admin";
}
}
通過@PreAuthorize
來進(jìn)行權(quán)限控制票彪,在hasAnyRole
中寫入訪問該api具有的權(quán)限(角色),除了可以使用@PreAuthorize
注解不狮,還可以使用@Secured
降铸、@PostAuthorize
注解
SpringSecurity框架的不足
對項(xiàng)目代碼有入侵
不夠通用,所有需要權(quán)限校驗(yàn)的系統(tǒng)都需要整合
SpringSecurity
框架摇零,不同應(yīng)用系統(tǒng)數(shù)據(jù)庫設(shè)計(jì)不同推掸,UserDetail
一般需自己實(shí)現(xiàn)【只是舉個(gè)例子,實(shí)際開發(fā)過程中重寫的類可能更多】角色應(yīng)該是動態(tài)的驻仅,但通過
SpringSecurity
配置的角色是靜態(tài)的谅畅,在數(shù)據(jù)庫新添角色時(shí)必須對代碼進(jìn)行修改,否則無法使用并不是一個(gè)
RBAC
的設(shè)計(jì)模型而是一個(gè)ACL
模型噪服,角色權(quán)限的劃分并不是特別清楚毡泻,權(quán)限也可以是角色,如果想基于RBAC
的權(quán)限校驗(yàn)就必須自己重新寫權(quán)限校驗(yàn)方法權(quán)限管理粒度不夠細(xì)粘优,比如沒法支持到方法級別的權(quán)限校驗(yàn)仇味,想支持更細(xì)粒度的權(quán)限必須自己寫權(quán)限校驗(yàn)
-
提供的三種緩存用戶信息的方式,分別為
NullUserCache
雹顺、EhCacheBasedUserCache
丹墨、SpringCacheBasedUserCache
。第一種永遠(yuǎn)return null
嬉愧,相當(dāng)于沒使用緩存贩挣,后兩者是內(nèi)存緩存,在分布式部署中會出現(xiàn)緩存命中率低、緩存不一致的情況揽惹,需要自己實(shí)現(xiàn)緩存僅是自己的一些看法被饿,如有紕漏歡迎指正
最后附:項(xiàng)目源碼