说说 Spring Security 账户权限体系

Spring Security 为这套账户体系提供了多种方案,

  1. 基于内存;

  2. 基于 JDBC;

  3. 基于 LDAP;

  4. 自定义服务。

不管使用哪种方法,都是通过覆盖 WebSecurityConfigurerAdapter 基础配置类中定义的 configure() 方法来实现的。

1 基于内存方式

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("deniro")
                .password("2382")
                .authorities("ROLE_USER")
                .and()
                .withUser("echo")
                .password("2382")
                .authorities("ROLE_USER");
    }
}

auth.inMemoryAuthentication() 会返回 InMemoryUserDetailsManagerConfigurer 对象,这个对象拥有 withUser() 方法,每次调用后都会配置一个用户放在内存中。withUser() 方法的入参是账户名,而密码和授权信息是通过 password() 和 authorities() 方法来指定的。

这种方式使用简单,适用于测试或简单的应用,但不方便维护。因为是硬编码,所以如果需要新增 、 删除或修改账户,就必须改代码,然后重新构建和部署应用。

2 基于 JDBC 方式

一般情况下,我们会把账户信息存储在数据库中,这时就会用到基于 JDBC 方式来配置账户。

(1)基础代码

基础代码模板如下:

    @Autowired
    DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource);
                
    }

这里使用注入方式来初始化 dataSource。

(2)基于角色的权限体系

springframework security 默认使用基于角色的权限体系的 5 张表来来存放账户、权限与角色信息。

从图中的关系中可以看出,权限不仅仅可以赋予给一个组,也可以赋予给一个账户。

如果应用中已经有权限体系,我们也可以自定义。可以自定义以下三条 SQL 语句。

  1. 依据账号查询账户(username,password,enabled);
  2. 依据账号查询权限(username,authority);
  3. 依据账号查询组名与权限(id, group_name, authority )。

这些语句可以在 JdbcDaoImpl.java 中看到实现 SQL:

public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
        + "from users " + "where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority "
        + "from authorities " + "where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority "
        + "from groups g, group_members gm, group_authorities ga "
        + "where gm.username = ? " + "and g.id = ga.group_id "
        + "and g.id = gm.group_id";

(3)自定义权限体系

JdbcUserDetailsManagerConfigurer 对象支持连缀编程,我们可以通过连缀编程来自定义这三条语句,形如:

 auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,enabled from Users where username=?"
                ).authoritiesByUsernameQuery(
                        "select username,authority from UserAuthorities where username=?"
        ).groupAuthoritiesByUsername("xxx");

注意:查询出来的字段名必须与默认语句的字段名一致。所以不是完全灵活的自定义,而是必须遵守一定的规范。

(4)加密存储

一般来说为了安全起见,数据库中的密码都必须是密文的。 Spring Security 已经考虑到了这一点。JdbcUserDetailsManagerConfigurer 类定义了一个 passwordEncoder() 方法,接受指定密码编码器作为入参。调用方式形如:

auth.jdbcAuthentication()
          .dataSource(dataSource)
          ...
          .passwordEncoder(new StandardPasswordEncoder("xxx"));

StandardPasswordEncoder 类在 Spring Boot2.x 中已经不再推荐使用了,因为已经不再安全。不用这个类也没关系,因为只要实现了 PasswordEncoder 接口的类,就可以作为 passwordEncoder() 方法的入参。

Security 的加密模块中定义很多这样的 PasswordEncoder 接口的实现类 。

  1. BCryptPasswordEncoder :使用 BCrypt 加密(推荐) 。
  2. NoOpPasswordEncoder :不进行任何转码 。
  3. Pbkdf2PasswordEncoder :使用 PBKDF2 加密(推荐) 。
  4. SCryptPasswordEncoder :使用 SCrypt 加密(推荐) 。

因为是单向加密,所以用户在登录时,会按照指定的算法,加密输入的密码,然后再与数据库中已经转码过的密码进行比对。这实际上会调用 PasswordEncoder 接口的 matches() 方法。该方法定义如下:

boolean matches(CharSequence rawPassword, String encodedPassword);

3 基于 LDAP 方式

(1)概念

LDAP 是轻型目录访问协议,它是是一个开放的,中立的,工业标准的应用协议,通过 IP 协议提供访问控制和维护分布式信息的目录信息。

目录服务是一种特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。它是动态的,灵活的,易扩展的。像人员组织管理,电话簿,地址簿等等就比较适合以目录的形式提供服务。

目录数据库和关系数据库不同,它有很好地读性能,但写性能差,并且没有事务处理、回滚等复杂功能,所以不适于存储修改频繁的数据。更适合应用与查询场景。

(2)基本用法

Spring Security 中,AuthenticationManagerBuilder 类定义了 ldapAuthentication() 方法支持 LDAP 认证方式。

首先在项目中的 pom.xml 中加入 spring-security-ldap 依赖包:

     <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-ldap</artifactId>
        </dependency>

如果未正确引入依赖,就会抛出 Caused by: java.lang.ClassNotFoundException: org.springframework.security.ldap.DefaultSpringSecurityContextSource

接着在 SecurityConfig 类的 configure() 方法中配置 LDAP 服务器:

auth.ldapAuthentication()
              .userSearchFilter("(uid={0})")
              .groupSearchFilter("member={0}");

ldapAuthentication() 方法会返回 LdapAuthenticationProviderConfigurer 类,它定义的 userSearchFilter() 与 groupSearchFilter() 方法,可分别用于查询账户与组。

默认情况下,会从 LDAP 层级结构的根开始查询。我们也可以指定查询起始点。

auth.ldapAuthentication()
              .userSearchBase("ou=apple")
              .userSearchFilter("(uid={0})")
              .groupSearchBase("ou=fruits")
              .groupSearchFilter("member={0}");

userSearchBase() 方法指定账户从名为 apple 的组织下开始查询;而 groupSearchBase() 方法则指定组从名为 fruits 的组织下开始查询。

(3)密码比对策略

LDAP 认证的默认策略是绑定,即直接通过 LDAP 服务器来认证账户。也可以改为采用密码比对策略。这就需要将输入的密码发送到 LDAP 目录上,LDAP 服务器会比对这个密码和账户的密码。因为只有 LDAP 服务器持有账户的密码,所以对应用来说,是不知道账户的真实密码的,这就进一步提升了账户密码的安全性。

改为采用密码比对策略很简单,只需要在 auth.ldapAuthentication() 之后加上
.passwordCompare() 方法即可:

 auth.ldapAuthentication()
                .userSearchBase("ou=apple")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=fruits")
                .groupSearchFilter("member={0}")
                //密码比对策略
                .passwordCompare();

默认情况下,用户输入的密码会与 LDAP 服务器中的 userPassword 属性进行比对。如果密码被定义在其它属性中,我们可以通过passwordAttribute() 方法来指定实际存放密码的属性名称。

auth.ldapAuthentication()
        .userSearchBase("ou=apple")
        .userSearchFilter("(uid={0})")
        .groupSearchBase("ou=fruits")
        .groupSearchFilter("member={0}")
        //密码比对策略
        .passwordCompare()
        //指定实际存放密码的属性名称
        .passwordAttribute("pwd");

(4)远程 LDAP 服务器

默认情况下, Spring Security 会监听本机的 33389 端口,即假设 LDAP 服务与应用服务在同一台机器上。可以通过 contextSource() 方法来配置远程 LDAP 服务器。

因为 contextSource() 方法返回的是 ContextSourceBuilder,所以不能把它挂在之前的连缀编码代码段下,必须另起炉灶。

LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> configurer = auth.ldapAuthentication();
...

//指定 LDAP 服务器
configurer.contextSource()
        .url("ldap://xxx:xxx");

(5)嵌入式 LDAP 服务器

利用 contextSource() 的 root() 方法就可以开启嵌入式 LDAP 服务器:

configurer.contextSource()
                .root("");

这时启动应用,就会在日志中看到启动的本地嵌入式 LDAP 服务器的 URL 地址:

当 LDAP 服务器启动时,它会在类路径下查找 LDIF 文件来加载数据。 LDIF ( LDAP Data Interchange Format)是 LDAP 服务器的数据交换格式。


关于如何自定义账户体系,会另开一篇文章详述。

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