Spring Security實現RBAC權限管理

Spring Security實現RBAC權限管理

1、簡介

在企業應用中,認證和受權是很是重要的一部份內容,業界最出名的兩個框架就是大名鼎鼎的 Shiro和Spring Security。因爲Spring Boot很是的流行,選擇Spring Security作認證和受權的 人愈來愈多,今天咱們就來看看用Spring 和 Spring Security如何實現基於RBAC的權限管理。html

2、基礎概念RBAC

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

3、集羣部署

爲了確保應用的高可用,通常都會將應用集羣部署。可是,Spring Security的會話機制是基於session的, 作集羣時對會話會產生影響。咱們在這裏使用Spring Session作分佈式Session的管理。node

4、技術選型

咱們使用的技術框架以下:mysql

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

5、具體實現

首先,咱們須要完成整個框架的整合,使用Spring Boot很是的方便,配置application.properties文件便可, 配置以下:git

#數據源配置
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,這裏不贅述。github

前面的這些都是準備工做,下面就要配置和使用Spring Security了,首先配置登陸的頁面和 密碼的規則,以及受權使用的技術實現等。咱們建立MyWebSecurityConfig繼承WebSecurityConfigurerAdapter ,並複寫configure方法,具體代碼以下:redis

@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上。可是,咱們大部分的業務場景都是登陸成功後,跳轉到登陸頁以前的 那個頁面,登陸頁以前的這個頁面是不定的。具體例子以下:spring

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

因此,這裏不須要指定登陸成功的url。sql

再來講說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

相關文章
相關標籤/搜索