说说 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 依赖包:

     
            org.springframework.security
            spring-security-ldap
        

如果未正确引入依赖,就会抛出 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 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 服务器的数据交换格式。


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

你可能感兴趣的:(说说 Spring Security 账户权限体系)