Spring Security 为这套账户体系提供了多种方案,
基于内存;
基于 JDBC;
基于 LDAP;
自定义服务。
不管使用哪种方法,都是通过覆盖 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 语句。
- 依据账号查询账户(username,password,enabled);
- 依据账号查询权限(username,authority);
- 依据账号查询组名与权限(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 接口的实现类 。
- BCryptPasswordEncoder :使用 BCrypt 加密(推荐) 。
- NoOpPasswordEncoder :不进行任何转码 。
- Pbkdf2PasswordEncoder :使用 PBKDF2 加密(推荐) 。
- 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 服务器的数据交换格式。
关于如何自定义账户体系,会另开一篇文章详述。