權限管理 【SpringSecurity篇】

只要和用戶打交道的系統基本都須要進行權限管理,否則哪一天操做不當給你刪庫了怎麼辦。開源的權限管理框架有SpringSecurity、Shiro,權限管理模型有RBAC、ACL等,是選擇開源框架好仍是基於權限管理模型造輪子好,必須都調研一下選一個適合公司業務的實現方式,首先先調研學習一波SpringSecurity

經過本篇文章你將瞭解到

  • SpringSecurity中的一些核心類
  • 使用SpringSecurity基於角色的權限校驗
  • SpringSecurity的不足

SpringSecurity核心類

由於對SpringSecurity只是調研性的學習,因此這裏並不會對源碼進行介紹。權限管理就是受權、鑑權,要想鑑權必須首先登錄拿到用戶信息,可是這裏也不會去講登錄、登出以及分佈式session管理,只會介紹一下鑑權過程當中的核心類,瞭解一下核心類能夠快速整合SpringSecurity框架。css

登錄校驗
  • UsernamePasswordAuthenticationFilter => 用戶登錄驗證,但這個類裏並不會正真去進行登錄校驗,而是經過ProviderManager
  • ProviderManager => 這個類裏有一個List<AuthenticationProvider>,提供了不一樣的校驗方式,只要其中一個經過便可。通常咱們什麼都不配的狀況下是根據用戶名和密碼,這時候AuthenticationProvider實現類爲AbstractUserDetailsAuthenticationProvider
  • AbstractUserDetailsAuthenticationProvider => 其登錄校驗是經過子類的additionalAuthenticationChecks方法完成的
  • DaoAuthenticationProvider => AbstractUserDetailsAuthenticationProvider的惟一子類,若是咱們設定的密碼(能夠基於內存也能夠基於數據庫)和傳過來的密碼比對不上登錄校驗失敗
  • UserDetailsService => 經過這個接口惟一的方法loadUserByUsername返回咱們設定的用戶名和密碼等用戶信息(被封裝爲UserDetails的實現類

小結:登錄驗證就是拿到客戶端的用戶名密碼和咱們設定的用戶名密碼(即UserDetails的實現類)進行比對,拿到咱們設定的用戶名密碼是經過UserDetailsService.loadUserByUsername(String userName)實現的[劃重點,一會要考],框架實現的UserDetailsService通常無法知足項目要求,就須要本身手動實現了,同時若是框架自帶的UserDetails的實現類無法知足要求咱們也能夠本身實現UserDetailsjava

權限校驗
  • FilterSecurityInterceptor => 基於角色的權限校驗攔截器,調用父類AbstractSecurityInterceptorbeforeInvocation方法進行鑑權
  • AbstractSecurityInterceptor => 調用AccessDecisionManager實現類的decide方法進行鑑權,因此如何想自定義鑑權方式能夠寫一個類而後實現AccessDecisionManager
  • AffirmativeBased => 默認使用的AccessDecisionManager實現類,調用AccessDecisionVoter實現類的vote方法進行鑑權,返回1權限校驗經過,其實跟蹤到最後其實仍是比對字符串不能說是投票吧,方法名容易讓人誤解
  • WebExpressionVoter => 默認使用的AccessDecisionVoter實現類,調用Authentication的authentication方法進行鑑權
  • SecurityExpressionOperations => 獲取Authentication對象接口
  • SecurityExpressionRoot => SecurityExpressionOperations實現類

小結:通常不自定鑑權方式的話這些咱們均可以不須要管,雖然層層調用了不少層,其實實質就是判斷當前的用戶所包含的權限列表中是否包含訪問指定url所須要的權限git

SpringBoot整合SpringSecurity

依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>1.4.1.RELEASE</version>
</dependency>
UserDetails實現類: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;
    }
}

實現類須要實現接口,這裏咱們將查詢到的roleList字符串封裝到SimpleGrantedAuthority中,SimpleGrantedAuthorityGrantedAuthority的一個實現類,若是默認實現無法知足需求可本身從新實現。UserDetailSpringSecurity和應用之間的橋樑,無論你數據庫怎麼建,只要你最後將用戶信息和權限的關係封裝爲UserDetailsSpringSecurity就能夠按它本身的機制進行權限校驗github

UserDetailsService實現類:UserDetailsServiceImpl.java
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UsersService usersService;

    /**
     * 根據用戶名獲取用戶信息
     */
    @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;
    }
}

該類做用就是將用戶信息和權限信息從數據庫查找到封裝爲一個UserDetails返回web

權限配置類:WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啓方法級安全驗證
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() //任何請求,登陸後能夠訪問
                .and().formLogin().permitAll(); //登陸頁面用戶任意訪問

        // 關閉CSRF跨域
        http.csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 設置攔截忽略文件夾,能夠對靜態資源放行
        web.ignoring().antMatchers("/css/**", "/js/**", "/templates/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //1.設置自定義userDetailService
        //2.校驗時指定密碼解碼方式
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}

這個類裏配置了登錄頁面以及一些簡單權限設置,好比任何請求登錄後可訪問、登陸頁面全部人能夠訪問。由於經過@EnableGlobalMethodSecurity註解開啓了方法級別驗證,在這個方法裏沒有配置方法級別的權限。同時經過指定本身實現的UserDetailsService以及密碼解碼方式,若是不指定密碼解碼方式將會報錯在最新的SpringSecurity版本中spring

控制層註解使用方式

@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來進行權限控制,在hasAnyRole中寫入訪問該api具備的權限(角色),除了可使用@PreAuthorize註解,還可使用@Secured@PostAuthorize註解數據庫

SpringSecurity框架的不足

  • 對項目代碼有入侵
  • 不夠通用,全部須要權限校驗的系統都須要整合SpringSecurity框架,不一樣應用系統數據庫設計不一樣,UserDetail通常需本身實現【只是舉個例子,實際開發過程當中重寫的類可能更多】
  • 角色應該是動態的,但經過SpringSecurity配置的角色是靜態的,在數據庫新添角色時必須對代碼進行修改,不然沒法使用
  • 並非一個RBAC的設計模型而是一個ACL模型,角色權限的劃分並非特別清楚,權限也能夠是角色,若是想基於RBAC的權限校驗就必須本身從新寫權限校驗方法
  • 權限管理粒度不夠細,好比無法支持到方法級別的權限校驗,想支持更細粒度的權限必須本身寫權限校驗
  • 提供的三種緩存用戶信息的方式,分別爲NullUserCacheEhCacheBasedUserCacheSpringCacheBasedUserCache。第一種永遠return null,至關於沒使用緩存,後二者是內存緩存,在分佈式部署中會出現緩存命中率低、緩存不一致的狀況,須要本身實現緩存

    僅是本身的一些見解,若有紕漏歡迎指正api

最後附:項目源碼跨域

相關文章
相關標籤/搜索