Spring Security實現RBAC權限管理


Spring Security實現RBAC權限管理

一、簡介

在企業應用中,認證和授權是非常重要的一部分內容,業界最出名的兩個框架就是大名鼎鼎的 Shiro和Spring Security。由於Spring Boot非常的流行,選擇Spring Security做認證和授權的 人越來越多,今天我們就來看看用Spring 和 Spring Security如何實現基於RBAC的權限管理。

二、基礎概念RBAC

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

三、集群部署

為了確保應用的高可用,一般都會將應用集群部署。但是,Spring Security的會話機制是基於session的, 做集群時對會話會產生影響。我們在這里使用Spring Session做分布式Session的管理。

四、技術選型

我們使用的技術框架如下:

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

五、具體實現

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

#數據源配置
spring.datasource.username=你的數據庫用戶名
spring.datasource.password=你的數據庫密碼
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生成對應的實體和DAO,這里不贅述。

前面的這些都是准備工作,下面就要配置和使用Spring Security了,首先配置登錄的頁面和 密碼的規則,以及授權使用的技術實現等。我們創建MyWebSecurityConfig繼承WebSecurityConfigurerAdapter ,並復寫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,並在類上標明注解@EnableWebSecurity,然后復寫configure方法, 由於我們的授權是采用注解方式的,所以這里只寫了authorizeRequests(),並沒有具體的授權信息。 接下來我們配置登錄url和登錄失敗的url,並沒有配置登錄成功的url,因為如果指定了登錄成功的url, 每次登錄成功后都會跳轉到這個url上。但是,我們大部分的業務場景都是登錄成功后,跳轉到登錄頁之前的 那個頁面,登錄頁之前的這個頁面是不定的。具體例子如下:

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

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

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

最后我們再類上標明注解@EnableGlobalMethodSecurity(prePostEnabled = true),這樣我們再 方法調用前會進行權限的驗證。

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

@Getter@Setter
private Set<SimpleGrantedAuthority> permissions;

其中SimpleGrantedAuthority是Spring Security提供的一個簡單的權限實體,它的構造函數只有一個 權限編碼的字符串,大多數情況下,我們這個權限類就夠用了。

然后,我們實現Spring Security的UserDetailsService1接口,完成用戶以及用戶權限的查詢, 代碼如下:

@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;
    }
}

這樣,用戶在登錄時就會調用這個方法,完成用戶以及用戶權限的查詢。

到此,用戶認證過程就結束了,登錄成功后,會跳到首頁或者登錄頁的前一頁(因為沒有配置登錄成功的url), 登錄失敗會跳到登錄失敗的url。

我們再看看權限判定的過程,我們在MyWebSecurityConfig類上標明了注解@EnableGlobalMethodSecurity(prePostEnabled = true),這使得我們 可以在方法上使用注解進行權限判定。我們在用戶登錄過程中查詢了用戶的權限,系統知道了用戶的權限,就可以進行權限的判定了。

我們看看方法上的權限注解,如下:

@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)指明具體的權限,其中xxx可以使用SPel表達式。如果不想指明具體的權限,僅僅使用 登錄、任何人等權限的,可以如下:

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

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

如果用戶不滿足指定的權限,會返回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的基本標簽,sec是Thymeleaf對Spring Security的擴展標簽,在頁面中我們進行權限的判定如下:

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

只有用戶在登錄的情況下,才可以顯示這個div下的內容。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM