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

相关文章
相关标签/搜索