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

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

一簡介

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

二、基礎(chǔ)概念RBAC

RBAC是Role Based Access Control的縮寫,是基于角色的訪問控制。一般都是分為用戶(user),
角色(role),權(quán)限(permission)三個(gè)實(shí)體,角色(role)和權(quán)限(permission)是多對多的
關(guān)系,用戶(user)和角色(role)也是多對多的關(guān)系。用戶(user)和權(quán)限(permission)
之間沒有直接的關(guān)系,都是通過角色作為代理,才能獲取到用戶(user)擁有的權(quán)限。一般情況下,
使用5張表就夠了,3個(gè)實(shí)體表,2個(gè)關(guān)系表。具體的sql清參照項(xiàng)目示例。

三、集群部署

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

四、技術(shù)選型

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

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

五、具體實(shí)現(xiàn)

首先,我們需要完成整個(gè)框架的整合,使用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)的實(shí)體和DAO,這里不贅述。

前面的這些都是準(zhǔn)備工作,下面就要配置和使用Spring Security了,首先配置登錄的頁面和
密碼的規(guī)則,以及授權(quán)使用的技術(shù)實(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,因?yàn)槿绻付说卿洺晒Φ膗rl,
每次登錄成功后都會(huì)跳轉(zhuǎn)到這個(gè)url上。但是,我們大部分的業(yè)務(wù)場景都是登錄成功后,跳轉(zhuǎn)到登錄頁之前的
那個(gè)頁面,登錄頁之前的這個(gè)頁面是不定的。具體例子如下:

  • 你在未登錄的情況下訪問了購物車頁,購物車頁需要登錄,跳轉(zhuǎn)到了登錄頁,登錄成功后你會(huì)返回購物車頁。
  • 你又在未登錄的情況下訪問了訂單詳情頁,訂單詳情頁需要登錄,跳轉(zhuǎn)到了登錄頁,登錄后你會(huì)跳轉(zhuǎn)到訂單詳情頁。

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

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

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

Spring Security提供的認(rèn)證方式有很多種,比如:內(nèi)存方式、LDAP方式。但是這些都和我們方式不符,
我們希望使用自己的用戶(User)來做認(rèn)證,Spring Security也提供了這樣的接口,方便了我們的開發(fā)。
首先,需要實(shí)現(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方法都是需要你自己實(shí)現(xiàn)的,其中有一個(gè)方法大家需要注意一下,那就是
getAuthorities()方法,它返回的是用戶具體的權(quán)限,在權(quán)限判定時(shí),需要調(diào)用這個(gè)方法。
所以我們再User類中定義了一個(gè)權(quán)限集合的變量

    @Getter@Setter
    private Set<SimpleGrantedAuthority> permissions;

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

然后,我們實(shí)現(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;
    }
}

這樣,用戶在登錄時(shí)就會(huì)調(diào)用這個(gè)方法,完成用戶以及用戶權(quán)限的查詢。

到此,用戶認(rèn)證過程就結(jié)束了,登錄成功后,會(huì)跳到首頁或者登錄頁的前一頁(因?yàn)闆]有配置登錄成功的url),
登錄失敗會(huì)跳到登錄失敗的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)限,會(huì)返回403錯(cuò)誤信息。

由于前段我們使用的是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>

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容