權(quán)限管理 【SpringSecurity篇】

只要和用戶打交道的系統(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)用父類AbstractSecurityInterceptorbeforeInvocation方法進(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中,SimpleGrantedAuthorityGrantedAuthority的一個(gè)實(shí)現(xiàn)類溜宽,如果默認(rèn)實(shí)現(xiàn)沒法滿足需求可自己重新實(shí)現(xiàn)吉拳。UserDetailSpringSecurity和應(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)目源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市搪搏,隨后出現(xiàn)的幾起案子狭握,更是在濱河造成了極大的恐慌,老刑警劉巖疯溺,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件论颅,死亡現(xiàn)場離奇詭異,居然都是意外死亡囱嫩,警方通過查閱死者的電腦和手機(jī)恃疯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墨闲,“玉大人今妄,你說我怎么就攤上這事≡П蹋” “怎么了盾鳞?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞻离。 經(jīng)常有香客問我腾仅,道長,這世上最難降的妖魔是什么套利? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任推励,我火速辦了婚禮,結(jié)果婚禮上肉迫,老公的妹妹穿的比我還像新娘验辞。我一直安慰自己,他們只是感情好昂拂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布受神。 她就那樣靜靜地躺著,像睡著了一般格侯。 火紅的嫁衣襯著肌膚如雪鼻听。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天联四,我揣著相機(jī)與錄音撑碴,去河邊找鬼。 笑死朝墩,一個(gè)胖子當(dāng)著我的面吹牛醉拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼亿卤,長吁一口氣:“原來是場噩夢啊……” “哼愤兵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起排吴,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤秆乳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后钻哩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屹堰,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年街氢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扯键。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡珊肃,死狀恐怖荣刑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情近范,我是刑警寧澤嘶摊,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站评矩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阱飘。R本人自食惡果不足惜斥杜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沥匈。 院中可真熱鬧蔗喂,春花似錦、人聲如沸高帖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽散址。三九已至乖阵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間预麸,已是汗流浹背瞪浸。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吏祸,地道東北人对蒲。 一個(gè)月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蹈矮。 傳聞我的和親對象是個(gè)殘疾皇子砰逻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容