前言
本文是常见Java Web应用中使用Spring Security的核心。一个Web应用管理系统在用户登陆上最核心的问题就是解决两个问题:1、用户提交的账号密码如何与数据库中保存的账号密码匹配上;2、如何保持会话。本文就是解决第一个问题的。
Spring Security自带存取方式
Spring Security自带了两种存取的方式:内存方式,JDBC方式。虽然实际应用中极少会用到这两种方式,但是可以先进行了解,因为后面讲述自定义方式时,也得先向这两种方式取经。
内存方式
内存方式是Spring Security的默认配置方式。如《Spring Security学习(一)——快速开始》建的应用,使用的就是内存方式。
我们可以进行如下配置,在WebSecurityConfig中配置:
@EnableWebSecurity
public class WebSecurityConfig{
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("abc").password("123").roles("USER").build());
manager.createUser(User.withDefaultPasswordEncoder().username("admin").password("456").roles("ADMIN").build());
return manager;
}
@Bean
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
第4-10行新建一个用内存方式管理的类,然后创建以默认密码加密方式创建两个账户,一个账号叫abc、密码123,,另一个账号叫admin、密码456。12-20行还是保持《Spring Security学习(四)——登陆认证(包括自定义登录页)》默认登陆页的方式。启动应用之后我们访问http://localhost:8080/hello路径,会重定向到登录页,我们尝试用admin/456去登陆:
登陆成功会跳到/hello路径下。
JDBC方式
JDBC方式其实应该称之为内嵌数据库方式。这种配置方式下Spring Security会从内置的数据库里存取相关的账号密码信息。
首先我们先在pom文件中增加对h2数据库的依赖,因为后面我们会用h2数据库作为内嵌数据库:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
我们配置WebSecurityConfig:
@EnableWebSecurity
public class WebSecurityConfig{
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build();
}
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}
@Bean
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
4-10行我们要配置内嵌数据库h2作为数据源,并配置其初始化数据库脚本JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION,即创建用户表、权限表。12-28行配置user和admin两个用户,密码用bcrypt加密了,密码为“password”。我们配置好后启动应用,访问/hello路径,然后提交账号密码admin/password验证登陆是否成功。
自定义UserDetailsService方式
终于来到重头戏了。现在的Java Web项目通常都是用自定义UserDetailsService的方式。这种方式需要做两件事:1、编写一个实现UserDetails接口的类;2、编写一个UserDetailsService接口的实现类。
UserDetails是什么?简单来说就是用来提供用户信息得接口。因为我们要用自定义的方式,所以系统需要知道我们自定义的用户信息怎么获取。
UserDetailsService是什么?是用来获取用户自定义数据的接口。我们要实现loadUserByUsername接口。例如我们的账号数据存在自己的数据库中,那么我们如何从数据库中取出用户信息,再把这些用户信息放到自定义的UserDetails实现类中。具体的实现方法参考《Spring Security学习(二)——使用数据库保存密码》即可,本文不再赘述。
自定义加密器
在“JDBC方式”一节中Sping Security使用了bcrypt的加密方式对密码进行加密,并在密文前加了前缀。如果我们不希望密码加上前缀的话,就要指定加密器。在《Spring Security学习(三)——密码加密》文中指定了Bcrypt加密器。那我们可以自定义自己的加密方式吗?当然可以了!
我们自定义一个密码器MyPasswordEncoder:
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword + "###";
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword.equals(rawPassword + "###")) {
return true;
}
return false;
}
}
其中encode方法表示加密的方式。MyPasswordEncoder采用的方式是再原密码的尾部加"###"。matches表示原密码和加密后的密码如何匹配。根据我们加密的方式,我们把原密码尾部加尾部加"###"后,再与加密后的密码进行匹配。
除此之外,PasswordEncoder接口其实还有一个upgradeEncoding方法,其解释是如果加密后的密码需要进一步加密就返回true,默认返回false。我一开始以为是会对密码进行二次加密,比如说密文是加"###",那upgradeEncoding返回true是不是等于加两次"###",那"123"对应的密文应该是"123######"。但经测试并非如此,如果我设置了加密后的密码为"123######",那输入"123"登陆是报错的!断点查看matches方法,与密码进行匹配的加密后密码只加了一次密!其实查看源码便知道一切了。upgradeEncoding是在密码匹配后进行的操作!!!因为被匹配上的用户信息(包括加密后的密码)会保存在Spring Security的上下文中,如果担心这个加密后密码还是不安全,upgradeEncoding返回true则会对加密后密码再次加密。
创建了MyPasswordEncoder之后,要在WebSecurityConfig创建它:
@EnableWebSecurity
public class WebSecurityConfig{
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build();
}
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("123###")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("456###")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
@Bean
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
上面的配置是沿用JDBC方式的,我们设置user用户的加密后密码为"123###",admin用户的加密后密码为"456###".启动程序,在登录页输入admin/456验证效果即可。
撬动UserDetailsService和PasswordEncoder的“杠杆”,DaoAuthenticationProvider
这一节和编码无关,单纯是探索一下Spring Security是如何调用UserDetailsService的。DaoAuthenticationProvider是AuthenticationProvider的默认实现方式。先看下图:
- 过滤器把用户提交的账号密码封装成UsernamePasswordAuthenticationToken提交过来;
- ProviderManager初始化时配置了AuthenticationProvider的实现类DaoAuthenticationProvider进行处理;
- DaoAuthenticationProvider通过UserDetailsService提取UserDetails;
- 用PasswordEncoder验证第3步提取的UserDetails与用户提交的密码是否匹配;
- 如果登录验证成功,则返回包含第三步UserDetails的UsernamePasswordAuthenticationToken。
小结
通过本文和 《Spring Security学习(四)——登陆认证(包括自定义登录页)》的介绍,用户已经基本可以搭建一套自定义的密码验证体系。不过这仅仅算是Spring Security最入门级的使用。另外本文介绍的UserDetails、UserDetailsService、DaoAuthenticationProvider也是Spring Security登陆认证的核心部分,很多商业应用为了搭建自己的登陆认证体系都会围绕这些内容进行扩展。