SpringSecurity会将用户的信息存储在SecurityContextHolder中,以下为SecurityContextHolder的模型
SecurityContextHolder中包含了SecurityContext对象,SecurityContext中包含了Authentication对象
Authentication对象的功能有两个:
Authentication包含三个属性:
AbstractAuthenticationProcessingFilter使用来进行用户认证的过滤器,会编排进SecurityFilterChain中,其处理流程为:
用户密码验证 DaoAuthenticationProvider
DaoAuthenticationProvider实现了接口AuthenticationProvider,并且会使用UserDetailsService和PasswordEncoder来验证用户和密码
在介绍SpringSecurity认证机制之前,先说一些关于密码存储的内容。在SpringSecurity中PasswordEncoder接口用来提供单向加密机制,使密码存储更加安全。
在过去的多年时间里,密码存储机制进化了多次。最开始,密码就是明文文本存储,例如存储在数据库中,获取密码则需要使用数据库的用户与密码,因此人们则认为这种方式是安全的,但一些恶意用户却可以轻易的破解此方式,例如使用SQL注入等手段。
此后,密码存储前会通过单向Hash加密,例如SHA-256,当一个用户申请认证时,系统会对用户输入的密码进行hash加密,然后与系统存储的加密密码进行比较,如此系统只需要存储加密后的密文,这样即使密码泄露,也不会泄露真实的密码。不幸的是,黑客们发明了彩虹表(Rainbow Tables),通过查询彩虹表,很多密码都可以轻易的破解。
为了降低彩虹表的能力,安全专家建议使用加盐密码(salted passwords),在密码进行单向hash加密时,加入一些随机字符,这些随机字符被称为盐(salt)。
但近些年来,随着计算机硬件的快速发展,hash运算速度已提升为每秒十亿级,这意味着单向hash加密机制已经不再安全。现在更加推荐使用自适应的单向加密机制,自适应的单向加密机制可以配置一个工作因子(work factor),该因子会跟随硬件设备性能自行调整,使得加密过程所需要的时间在一个固定的值,这个值推荐在1秒钟左右,如此破解密码变得更加困难,常见的自适应加密机制包括bcrypt, PBKDF2, scrypt, 和 argon2
在Spring Security 5.0以前,默认的PasswordEncoder是NoOpPasswordEncoder,即明文密码,现在Spring Security的默认PasswordEncoder是DelegatingPasswordEncoder,DelegatingPasswordEncoder可以代理多种加密机制,其实例化过程如下:
public class DelegatingPasswordEncoderTest {
public static void main(String[] args) {
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
//对密码进行加密
String encodedPassword = passwordEncoder.encode("password");
System.out.println(encodedPassword); //{bcrypt}$2a$10$8nfdxLtGwvDu4MRI.CpCmOg56d7zNyq2xVJqaO4b.OPDFjlsu47Am
//验证密码
boolean res = passwordEncoder.matches("password", "{bcrypt}$2a$10$550FMm3qENbQtIWOa6qfsuqBftBF3BfVibViHv2ueQ3wGs45jEPWu");
System.out.println("res = " + res); // res = true
}
}
密码的存储格式为,其中{id}为对应的加密算法:
{id}encodedPassword
以下为对字符串"password"加密后的各种算法的结果:
{bcrypt}$2a$10$4lVeyJaX4NeUGLcAOG1KsuhguK30dwxMt9vO4woC.5elEC2xufWSu
{noop}password
{pbkdf2}8c3682ac6c8a7285060cc984fe3cf6da2af1a98410ad244b8b2b90fb90f60665c437a5cff05682e3
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}9b050796bcdab7d9f9009669fac770584071e4057f4edae723e28096fe21910e7e95420647a2749f
在Springboot中引入Spring Security依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
启动服务,Spring Scurity就会自动创建一个页面登陆身份认证,默认的用户为user,密码会在启动日志中打印出来
输入任何地址,就会跳转至默认的登陆页面,输入用户密码后可以实现登陆。
自定义页页面配置:HttpSecurity .formLogin().loginPage("/login") 指定了自定义的登陆页面,关于授访问控制相关的配置会在以后的章节进行介绍
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login");
}
}
创建登陆页面的Controller
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
return "login";
}
}
创建登陆页面,这里我使用的Themeleaf
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Please Log Intitle>
head>
<body>
<div style="width:500px;margin:0 auto">
<h1>Please Log Inh1>
<div th:if="${param.error}">
Invalid username and password.
div>
<div th:if="${param.logout}">
You have been logged out.
div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username">
div>
<div>
<input type="password" name="password" placeholder="Password">
div>
<input type="submit" value="Login">
form>
div>
body>
html>
Basic Authentication是针对服务接口的身份认证,当用户的请求中没有携带用户/密码信息,或者密码不匹配时,Spring Security会在Http的response中增加 WWW-Authenticate头信息,客户端根据这个头信息判断是否认证通过。
Basic Authentication开启配置
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login") //指定自定义登陆页面
.and()
.httpBasic(); //开启Basic Authentication
}
}
开发一个restful接口进行验证
@Controller
public class LoginController {
@RequestMapping("/basic")
@ResponseBody
public String basic() {
return "hello";
}
}
使用Postman发送get请求至接口,当不填写用户密码或者错误密码时,可以看到返回信息头中包含WWW-Authenticate
当填写用户密码信息后则可以正常访问。
InMemoryUserDetailsManager实现接口UserDetailsManager, UserDetailsManager接口继承UserDetailsService接口,支持查询存储在内存中的用户密码信息,也提供了堆UserDetails的管理
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login") //指定自定义登陆页面
.and()
.httpBasic(); //开启Basic Authentication
}
/**
* 用户密码存储在内存中
* @return
*/
@Bean
public UserDetailsService inMemoryUserDetailsManager() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
以上代码中使用的是Spring Security默认的PasswordEncoder
此种方式在维护用户信息时需要修改源代码并编译,有些繁琐,所以往往被使用在项目初期的验证阶段,并不推荐使用在正式的产品中。
JdbcDaoImpl 实现接口UserDetailsService,可以通过JDBC查询用户密码信息
JdbcUserDetailsManager 继承了JdbcDaoImpl ,同时实现了UserDetailsManager,提供对UserDetails的管理
(1)根据官方文档创建用户表,这里使用PostgreSQL数据库
create table users(
username varchar(50) not null primary key,
password varchar(200) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(200) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
如果使用了用户组,还需要创建组信息表
create table groups (
id bigint generated by default as identity(start with 1) primary key,
group_name varchar(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 1) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
(2)创建数据源
这里使用的PostgreSQL数据库
首先添加maven依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.postgresqlgroupId>
<artifactId>postgresqlartifactId>
dependency>
在application.yml中配置数据库
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: daiwl
driver-class-name: org.postgresql.Driver
(3) 配置JdbcUserDetailsManager
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource; //注入数据源
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login") //指定自定义登陆页面
.and()
.httpBasic(); //开启Basic Authentication
}
/**
* 用户密码存储在内存中
* @return
*/
// @Bean
// public UserDetailsService inMemoryUserDetailsManager() {
// UserDetails user = User.builder()
// .username("user")
// .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
// .roles("USER")
// .build();
//
// UserDetails admin = User.builder()
// .username("admin")
// .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
// .roles("USER", "ADMIN")
// .build();
// return new InMemoryUserDetailsManager(user, admin);
// }
/**
* JDBC Authentication
* @param dataSource
* @return
*/
@Bean
public UserDetailsManager users(DataSource dataSource) {
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
//创建user和admin用户
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
.roles("USER")
.build();
UserDetails jdbc= User.builder()
.username("jdbc")
.password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
.roles("USER", "JDBC")
.build();
users.deleteUser("user");
users.createUser(user);
users.deleteUser("jdbc");
users.createUser(jdbc);
return users;
}
}
以上代码在配置JdbcUserDetailsManager之后,立即创建了user和jdbc用户
用户也可以通过实现UserDetailsService接口自定义身份认证,Spring Security提供了以上两种实现,In-memory和jdbc,如果不能满足需求,则可以自行定义:
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}