Spring Security實現(xiàn)RBAC權(quán)限管理

Spring Security實現(xiàn)RBAC權(quán)限管理

一簡介

在企業(yè)應(yīng)用中,認(rèn)證和授權(quán)是非常重要的一部分內(nèi)容,業(yè)界最出名的兩個框架就是大名鼎鼎的
Shiro和Spring Security窖铡。由于Spring Boot非常的流行,選擇Spring Security做認(rèn)證和授權(quán)的
人越來越多统捶,今天我們就來看看用Spring 和 Spring Security如何實現(xiàn)基于RBAC的權(quán)限管理七蜘。

二、基礎(chǔ)概念RBAC

RBAC是Role Based Access Control的縮寫捕仔,是基于角色的訪問控制匕积。一般都是分為用戶(user),
角色(role)榜跌,權(quán)限(permission)三個實體闪唆,角色(role)和權(quán)限(permission)是多對多的
關(guān)系,用戶(user)和角色(role)也是多對多的關(guān)系钓葫。用戶(user)和權(quán)限(permission)
之間沒有直接的關(guān)系悄蕾,都是通過角色作為代理,才能獲取到用戶(user)擁有的權(quán)限础浮。一般情況下帆调,
使用5張表就夠了,3個實體表豆同,2個關(guān)系表贷帮。具體的sql清參照項目示例。

三诱告、集群部署

為了確保應(yīng)用的高可用撵枢,一般都會將應(yīng)用集群部署民晒。但是,Spring Security的會話機(jī)制是基于session的锄禽,
做集群時對會話會產(chǎn)生影響潜必。我們在這里使用Spring Session做分布式Session的管理。

四沃但、技術(shù)選型

我們使用的技術(shù)框架如下:

  • Spring Boot
  • Spring Security
  • Spring Data Redis
  • Spring Session
  • Mybatis-3.4.6
  • Druid
  • Thymeleaf(第一次使用)

五磁滚、具體實現(xiàn)

首先,我們需要完成整個框架的整合宵晚,使用Spring Boot非常的方便垂攘,配置application.properties文件即可,
配置如下:

#數(shù)據(jù)源配置
spring.datasource.username=你的數(shù)據(jù)庫用戶名
spring.datasource.password=你的數(shù)據(jù)庫密碼
spring.datasource.url=jdbc:mysql://localhost:3306/security_rbac?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai

#mybatis配置
#mybatis.mapper-locations=mybatis/*.xml
#mybatis.type-aliases-package=com.example.springsecurityrbac.model

#redis配置
#spring.redis.cluster.nodes=149.28.37.147:7000,149.28.37.147:7001,149.28.37.147:7002,149.28.37.147:7003,149.28.37.147:7004,149.28.37.147:7005
spring.redis.host=你的redis地址
spring.redis.password=你的redis密碼

#spring-session配置
spring.session.store-type=redis
#thymeleaf配置
spring.thymeleaf.cache=false

然后淤刃,使用Mybatis Generator生成對應(yīng)的實體和DAO晒他,這里不贅述。

前面的這些都是準(zhǔn)備工作逸贾,下面就要配置和使用Spring Security了陨仅,首先配置登錄的頁面和
密碼的規(guī)則,以及授權(quán)使用的技術(shù)實現(xiàn)等铝侵。我們創(chuàng)建MyWebSecurityConfig繼承WebSecurityConfigurerAdapter
灼伤,并復(fù)寫configure方法,具體代碼如下:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .and()
                .formLogin()
                .loginPage("/login").failureForwardUrl("/login-error")
//                .successForwardUrl("/index")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

}

我們繼承WebSecurityConfigurerAdapter咪鲜,并在類上標(biāo)明注解@EnableWebSecurity狐赡,然后復(fù)寫configure方法,
由于我們的授權(quán)是采用注解方式的疟丙,所以這里只寫了authorizeRequests()颖侄,并沒有具體的授權(quán)信息。
接下來我們配置登錄url和登錄失敗的url隆敢,并沒有配置登錄成功的url发皿,因為如果指定了登錄成功的url崔慧,
每次登錄成功后都會跳轉(zhuǎn)到這個url上拂蝎。但是,我們大部分的業(yè)務(wù)場景都是登錄成功后惶室,跳轉(zhuǎn)到登錄頁之前的
那個頁面温自,登錄頁之前的這個頁面是不定的。具體例子如下:

  • 你在未登錄的情況下訪問了購物車頁皇钞,購物車頁需要登錄悼泌,跳轉(zhuǎn)到了登錄頁,登錄成功后你會返回購物車頁夹界。
  • 你又在未登錄的情況下訪問了訂單詳情頁馆里,訂單詳情頁需要登錄,跳轉(zhuǎn)到了登錄頁,登錄后你會跳轉(zhuǎn)到訂單詳情頁鸠踪。

所以丙者,這里不需要指定登錄成功的url。

再來說說PasswordEncoder這個Bean营密,Spring Security掃描到PasswordEncoder這個Bean械媒,
就會把它作為密碼的加密規(guī)則,這個我們使用NoOpPasswordEncoder评汰,沒有密碼加密規(guī)則纷捞,數(shù)據(jù)庫中
存的是密碼明文。如果需要其他加密規(guī)則可以參考PasswordEncoder的實現(xiàn)類被去,也可以自己實現(xiàn)
PasswordEncoder接口主儡,完成自己的加密規(guī)則。

最后我們再類上標(biāo)明注解@EnableGlobalMethodSecurity(prePostEnabled = true)编振,這樣我們再
方法調(diào)用前會進(jìn)行權(quán)限的驗證缀辩。

Spring Security提供的認(rèn)證方式有很多種,比如:內(nèi)存方式踪央、LDAP方式臀玄。但是這些都和我們方式不符,
我們希望使用自己的用戶(User)來做認(rèn)證畅蹂,Spring Security也提供了這樣的接口健无,方便了我們的開發(fā)。
首先液斜,需要實現(xiàn)Spring Security的UserDetails接口累贤,代碼如下:

public class User implements UserDetails {
    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private Integer id;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private String username;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private String password;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    private Boolean locked;

    @Getter@Setter
    private Set<SimpleGrantedAuthority> permissions;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public Integer getId() {
        return id;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setId(Integer id) {
        this.id = id;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return permissions;
    }

    public void setAuthorities(Set<SimpleGrantedAuthority> permissions){
        this.permissions = permissions;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public String getPassword() {
        return password;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public Boolean getLocked() {
        return locked;
    }

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public void setLocked(Boolean locked) {
        this.locked = locked;
    }
}

其中所有的@Override方法都是需要你自己實現(xiàn)的,其中有一個方法大家需要注意一下少漆,那就是
getAuthorities()方法臼膏,它返回的是用戶具體的權(quán)限,在權(quán)限判定時示损,需要調(diào)用這個方法渗磅。
所以我們再User類中定義了一個權(quán)限集合的變量

    @Getter@Setter
    private Set<SimpleGrantedAuthority> permissions;

其中SimpleGrantedAuthority是Spring Security提供的一個簡單的權(quán)限實體,它的構(gòu)造函數(shù)只有一個
權(quán)限編碼的字符串检访,大多數(shù)情況下始鱼,我們這個權(quán)限類就夠用了。

然后脆贵,我們實現(xiàn)Spring Security的UserDetailsService1接口医清,完成用戶以及用戶權(quán)限的查詢,
代碼如下:

@Service
public class SecurityUserService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SelectStatementProvider selectStatement = select(UserDynamicSqlSupport.id,UserDynamicSqlSupport.username,UserDynamicSqlSupport.password,UserDynamicSqlSupport.locked)
                .from(UserDynamicSqlSupport.user)
                .where(UserDynamicSqlSupport.username,isEqualTo(username))
                .build().render(RenderingStrategy.MYBATIS3);

        Map<String,Object> parameter = new HashMap<>();
        parameter.put("#{username}",username);
        User user = userMapper.selectOne(selectStatement);
        if (user == null) throw new UsernameNotFoundException(username);

        SelectStatementProvider manyPermission = select(PermissionDynamicSqlSupport.id,PermissionDynamicSqlSupport.permissionCode,PermissionDynamicSqlSupport.permissionName)
                .from(PermissionDynamicSqlSupport.permission)
                .join(RolePermissionDynamicSqlSupport.rolePermission).on(RolePermissionDynamicSqlSupport.permissionId,equalTo(PermissionDynamicSqlSupport.id))
                .join(UserRoleDynamicSqlSupport.userRole).on(UserRoleDynamicSqlSupport.roleId,equalTo(RolePermissionDynamicSqlSupport.roleId))
                .where(UserRoleDynamicSqlSupport.userId,isEqualTo(user.getId()))
                .build()
                .render(RenderingStrategy.MYBATIS3);
        List<Permission> permissions = permissionMapper.selectMany(manyPermission);
        if (!CollectionUtils.isEmpty(permissions)){
            Set<SimpleGrantedAuthority> sga = new HashSet<>();
            permissions.forEach(p->{
                sga.add(new SimpleGrantedAuthority(p.getPermissionCode()));
            });
            user.setAuthorities(sga);
        }

        return user;
    }
}

這樣卖氨,用戶在登錄時就會調(diào)用這個方法会烙,完成用戶以及用戶權(quán)限的查詢负懦。

到此,用戶認(rèn)證過程就結(jié)束了柏腻,登錄成功后密似,會跳到首頁或者登錄頁的前一頁(因為沒有配置登錄成功的url),
登錄失敗會跳到登錄失敗的url葫盼。

我們再看看權(quán)限判定的過程残腌,我們在MyWebSecurityConfig類上標(biāo)明了注解@EnableGlobalMethodSecurity(prePostEnabled = true),這使得我們
可以在方法上使用注解進(jìn)行權(quán)限判定贫导。我們在用戶登錄過程中查詢了用戶的權(quán)限抛猫,系統(tǒng)知道了用戶的權(quán)限,就可以進(jìn)行權(quán)限的判定了孩灯。

我們看看方法上的權(quán)限注解闺金,如下:

    @PreAuthorize("hasAuthority(T(com.example.springsecurityrbac.config.PermissionContact).USER_VIEW)")
    @RequestMapping("/user/index")
    public String userIndex() {
        return "user/index";
    }

這是我們在Controller中的一段代碼,使用注解@PreAuthorize("hasAuthority(xxx)")峰档,其中我們使用
hasAuthority(xxx)指明具體的權(quán)限败匹,其中xxx可以使用SPel表達(dá)式。如果不想指明具體的權(quán)限讥巡,僅僅使用
登錄掀亩、任何人等權(quán)限的,可以如下:

  • isAnonymous()
  • isAuthenticated()
  • isRememberMe()

還有其他的一些方法欢顷,請Spring Security官方文檔槽棍。

如果用戶不滿足指定的權(quán)限,會返回403錯誤信息抬驴。

由于前段我們使用的是Thymeleaf炼七,它對Spring Security的支持非常好,我們在pom.xml中添加如下配置:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.2.RELEASE</version>
</dependency>

并在頁面中添加如下引用:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
      ........
</html>

th是Thymeleaf的基本標(biāo)簽布持,sec是Thymeleaf對Spring Security的擴(kuò)展標(biāo)簽豌拙,在頁面中我們進(jìn)行權(quán)限的判定如下:

<div class="logout" sec:authorize="isAuthenticated()">
    ............
</div>

只有用戶在登錄的情況下,才可以顯示這個div下的內(nèi)容题暖。

到此按傅,Spring Security就給大家介紹完了,具體的項目代碼參照我的GitHub地址:
https://github.com/liubo-tech/spring-security-rbac

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芙委,一起剝皮案震驚了整個濱河市逞敷,隨后出現(xiàn)的幾起案子狂秦,更是在濱河造成了極大的恐慌灌侣,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裂问,死亡現(xiàn)場離奇詭異侧啼,居然都是意外死亡牛柒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門痊乾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來皮壁,“玉大人,你說我怎么就攤上這事哪审《昶牵” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵湿滓,是天一觀的道長滴须。 經(jīng)常有香客問我,道長叽奥,這世上最難降的妖魔是什么扔水? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮朝氓,結(jié)果婚禮上魔市,老公的妹妹穿的比我還像新娘。我一直安慰自己赵哲,他們只是感情好待德,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著枫夺,像睡著了一般磅网。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筷屡,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天涧偷,我揣著相機(jī)與錄音,去河邊找鬼毙死。 笑死燎潮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扼倘。 我是一名探鬼主播确封,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼再菊!你這毒婦竟也來了爪喘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤纠拔,失蹤者是張志新(化名)和其女友劉穎秉剑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稠诲,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡侦鹏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了价卤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跨释。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疙驾,到底是詐尸還是另有隱情它碎,我是刑警寧澤乘碑,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布资铡,位于F島的核電站尖飞,受9級特大地震影響底洗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摧扇,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滑负。 院中可真熱鬧在张,春花似錦、人聲如沸矮慕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痴鳄。三九已至瘟斜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痪寻,已是汗流浹背螺句。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工蟹倾, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留碾局,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓祟敛,卻偏偏與公主長得像猫态,于是被迫代替她去往敵國和親佣蓉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354

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