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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,501评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,673评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,610评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,939评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,668评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,004评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,001评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,173评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,705评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,426评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,656评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,139评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,833评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,247评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,580评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,371评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,621评论 2 380

推荐阅读更多精彩内容