简单的开始
创建SpringBoot项目
首先创建一个简单的springboot项目,假设端口为8888,添加controller控制层,并在其中添加TestController
控制类,那么启动springboot项目之后,访localhost:8888/api/message
页面会显示my first message
java
@RestController
@RequestMapping("/api")
public TestController{
@GetMapping("/messages")
public String myMessage(){
return "my first message";
}
}
添加SpringSecurity的依赖
xml
<dependencies>
<!-- ... 其他依赖元素 ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<!--可以通过下面的内容进行版本指定-->
<spring-security.version>6.2.0-SNAPSHOT</spring-security.version>
</dependency>
</dependencies>
SpringSecurity认证登录
运行springboot项目后,在控制台输出窗口出现:
bash
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
尝试访问localhost:port
任意后端接口地址,可以发现出现了登录窗口,
使用user: user
password:8e557245-73e2-4286-969a-ff57fe326336
这里的密码就是控制台输出的密码。
这就是springsecurity的端口认证机制。
原理说明
Filter和FilterChain
当客户端向应用程序发送请求时,SpringSecurity会创建一系列的Filter
来过滤请求,这样的Filter
有多个,这些Filter
构成了从客户端到Servlet的一个FilterChain
,在通过FilterChain
的过滤之后,这个请求才会被Servlet处理。
需要注意的是Filter
会影响下游的 Filter
实例,当匹配到一个Filter
之后就不再匹配下面的Filter
流程如下所示。

过滤过程的伪代码
java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 在过滤之前的动作
chain.doFilter(request, response); // 进行过滤
// 过滤之后的动作
}
FilterChainProxy和SecurityFilterChain
基本流程
当一个请求来临时,我们常常会有这样的动作,对于某一个请求接口,去查看对应的过滤器,比如对account相关的接口,我们就会给account制定对应的过滤器,当account相关的请求来临时,我们必然的就需要去通过account的过滤器去处理该请求。
FilterChainProxy
就是这样的一个角色, 用来确定当前请求应该调用哪些 Spring Security Filter
实例。
SecurityFilterChain
的作用就是将过滤器进行分类,用来被FilterChainProxy
识别调用。

因此当设计多个接口过滤器时,基本架构如下图所示

举例说明
FilterChainProxy
决定应该使用哪个 SecurityFilterChain
。只有第一个匹配的 SecurityFilterChain
被调用。
- 如果请求的URL是
/api/messages
,它首先与/api/**
的SecurityFilterChain0
模式匹配,所以只有SecurityFilterChain0
被调用,尽管它也与SecurityFilterChainn
匹配。 - 如果请求的URL是
/messages
,它与/api/**
的SecurityFilterChain_0
模式不匹配,所以FilterChainProxy
会继续顺序尝试下面的SecurityFilterChain
。假设没有其他SecurityFilterChain
实例相匹配,则调用SecurityFilterChain_n
。
工作流程
是 否 是 否 是 否 客户端请求受保护资源 Spring Security 过滤器链拦截请求 UserDetailsServiceAutoConfiguration 配置 AuthenticationManager 需要认证? UsernamePasswordAuthenticationFilter 拦截 访问资源 DaoAuthenticationProvider 处理认证 使用自定义 UserDetailsService? 自定义 UserDetailsService 加载用户信息 InMemoryUserDetailsManager 加载默认用户信息 密码匹配? 创建 Authentication 对象 抛出 AuthenticationException 将 Authentication 对象放入 SecurityContextHolder 访问资源 重定向到登录页面或返回错误
1. 自动配置的过程
-
UserDetailsServiceAutoConfiguration
类上的条件注解-
@ConditionalOnClass(AuthenticationManager.class)
确保
AuthenticationManager
类在类路径上。 -
@ConditionalOnBean(ObjectPostProcessor.class)
确保 Spring 容器中存在
ObjectPostProcessor
的 Bean -
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
确保 Spring 容器中没有定义
AuthenticationManager
、AuthenticationProvider
和UserDetailsService
的 Bean。
-
-
默认InMemoryUserDetailsManager 的Bean 的创建
如果上述条件都满足,
UserDetailsServiceAutoConfiguration
会创建一个InMemoryUserDetailsManager
的 Bean 作为默认的用户详细信息服务管理器。这个管理器会在内存中创建一个用户,通常用户名为 "user",密码为随机生成的 UUID,这个角色为 "USER"。在创建
InMemoryUserDetailsManager
时,UserDetailsServiceAutoConfiguration
会检查SecurityProperties
中定义的用户密码。如果密码是生成的,它会记录一条日志,显示使用的密码。同时,它还会检查密码是否已经使用某种算法进行了编码,如果没有,它会使用{noop}
前缀,表示密码没有被编码。创建过程代码:可以简单浏览,之后自己创建配置时会借鉴到
java@Bean public InMemoryUserDetailsManager inMemoryUserDetailsManager( SecurityProperties properties,// springsecurity的配置文件,里面有默认的用户名,可以进入 SecurityProperties 查看详细数据 ObjectProvider<PasswordEncoder> passwordEncoder) // 密码编码器 { SecurityProperties.User user = properties.getUser(); // 配置文件中的用户 List<String> roles = user.getRoles(); // 获取角色 return new InMemoryUserDetailsManager( new UserDetails[]{ User.withUsername(user.getName()) // 账号 .password( // 密码 this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable()) ) .roles(StringUtils.toStringArray(roles)) //角色 .build() } ); }
-
注册Bean
最终,
UserDetailsServiceAutoConfiguration
会将InMemoryUserDetailsManager
注册为 Spring 应用上下文中的一个 Bean,这样 Spring Security 在认证时就可以使用这个默认的用户详细信息服务。
2. UserDetailsService的作用
我们通过上面InMemoryUserDetailsManager的类,可以分析得出
-
InMemoryUserDetailsManager实现了UserDetailsManager、UserDetailsPasswordService中的方法
-
UserDetailsManager继承UserDetailsService
因此InMemoryUserDetailsManager的关键就是UserDetailsManager
、UserDetailsPasswordService
以及实现自UserDetailsService
中的方法
接口 | 方法 | 描述 |
---|---|---|
UserDetailsManager | void createUser(UserDetails user) |
根据提供的用户详情创建一个新用户账号 |
void updateUser(UserDetails user) |
更新指定的用户账号 | |
void deleteUser(String username) |
从系统中删除具有给定登录名的用户账号 | |
void changePassword(String oldPassword, String newPassword) |
修改用户账号的密码。这应该在持久的用户存储库中更改用户的密码(数据库、LDAP等) | |
boolean userExists(String username) |
检查具有给定登录名的用户账号是否存在于系统中 | |
UserDetails loadUserByUsername(String username) |
根据用户名加载用户信息,此方法从 UserDetailsService 继承 |
|
UserDetailsPasswordService | UserDetails updatePassword(UserDetails user, String newPassword) |
更新用户密码。在用户登录成功后,如果检测到密码需要更新(例如,密码策略变更),则调用此方法 |
而我们可以通过上面部分自动配置过程
可以知道,假如Spring 容器中定义了 AuthenticationManager
、AuthenticationProvider
和 UserDetailsService
的 Bean,那么自动配置文件将不会生效。
3. AuthenticationManager的作用
在Spring Security中,AuthenticationManager
是一个核心接口,负责对用户的认证请求进行处理。它定义了一个 authenticate
方法,该方法接受一个 Authentication
对象作为参数,并返回一个完全认证过的 Authentication
对象。如果认证失败,则抛出 AuthenticationException
。
ProviderManager
是 AuthenticationManager
的一个常见实现,它使用一个 AuthenticationProvider
列表来处理认证请求。每个 AuthenticationProvider
都有机会对认证请求进行处理,如果一个 AuthenticationProvider
无法处理请求,ProviderManager
会尝试下一个。这个过程会一直持续,直到找到一个能够成功认证请求的 AuthenticationProvider
,或者所有的 AuthenticationProvider
都尝试完毕。
找到匹配的 Provider 认证成功 认证失败 未找到匹配的 Provider 开始认证 AuthenticationManager ProviderManager 遍历 AuthenticationProvider 列表 Provider 进行认证 返回认证后的 Authentication 对象 抛出 AuthenticationException 抛出 ProviderNotFoundException 认证完成
4. 手动配置账号密码
1)创建配置类、用户管理器
因此我们创建自己的WebSecurityConfig
类 ,在里面进行InMemoryUserDetailsManager
的注入,并实现构造方法。这样我们就手动创建了自己的配置内容。
java
@Configuration
public class WebSecurityConfig {
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
return new InMemoryUserDetailsManager();
}
}
当然我们里面还没有给InMemoryUserDetailsManager
添加任何用户。
2)初始化用户
添加下面代码,在创建InMemoryUserDetailsManager
时新建一个用户
java
@Configuration
public class WebSecurityConfig {
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
return new InMemoryUserDetailsManager(
User.withUsername("user") // 用户名
.password("{noop}password") // 密码,以{noop}开头的话代表不加密
.roles("a") // 使用可变参数传递角色
.build()
);
}
}
这样,当我们启动时,就可以根据上面的账号和密码进行登录
3)添加用户
当然我们也可以通过调用InMemoryUserDetailsManager
中的createUser
方法添加用户的方式,来初始化manager用户管理器,下面我们展示创建两个用户的过程。
java
@Configuration
public class WebSecurityConfig {
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(new User("admin", "{noop}123456", List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));
manager.createUser(new User("user", "{noop}654321", List.of(new SimpleGrantedAuthority("ROLE_USER"))));
return manager;
}
}
4)认证过程
我们通过上面的内容已经知道了初始化用户
添加用户
,同样的里面的updateUser
deleteUser
changePassword
userExists
方法也基本类似,不再赘述。
接下来需要弄懂的就是如何认证的呢,我们明明没有写这些相关的方法。
通过最开始的流程图,我们可以知道在配置好认证用户之后,之后程序对于每一个请求都会进行拦截。

请求拦截
AbstractAuthenticationProcessingFilter
将请求拦截,并通过调用attemptAuthentication
方法进行处理,而这个方法的具体实现存在于UsernamePasswordAuthenticationFilter
中
将请求进行拦截,然后交给授权管理器AuthenticationManager
进行控制
授权管理器认证
进入authenticate()方法发现进入到一个AuthenticationManager
接口中,而这个接口的实现类是ProviderManager

在ProviderManager
类中的authenticate
方法委派认证工作给一个或多个AuthenticationProvider
验证用户是否存在
AuthenticationProvider
仍为一个接口,其默认实现类为AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
的authenticate
方法流程如下:
- 开始认证:认证过程开始。
- 检查Authentication类型 :确保传入的
Authentication
对象是UsernamePasswordAuthenticationToken
类型。 - 抛出异常:如果类型不匹配,抛出异常。
- 确定用户名 :从
Authentication
对象中获取用户名。 - 从Cahce缓存获取UserDetails :尝试从Cahce缓存中获取
UserDetails
对象。 - Cahce未命中:如果Cahce未命中,从用户信息源(如数据库)检索用户信息。
- 用户不存在 :如果用户不存在,根据配置抛出
UsernameNotFoundException
或BadCredentialsException
。 - 用户存在:如果用户存在,校验用户状态(如账户是否过期、是否锁定等)。
- 用户状态无效 :如果用户状态无效,抛出
AuthenticationException
。 - 执行额外的认证检查:执行任何额外的认证检查(如密码过期检查)。
- 认证检查失败:如果认证检查失败,重新检索用户信息并再次执行检查。
- 执行后置认证检查:执行认证成功后的后置检查。
- 后置检查失败 :如果后置检查失败,抛出
AuthenticationException
。 - 检查是否使用缓存:检查认证过程中是否使用了缓存。
- 使用了缓存:如果没有使用缓存,将用户信息放入缓存。
- 创建认证成功的Authentication对象 :创建一个新的
Authentication
对象,表示认证成功。 - 返回认证成功的Authentication对象 :返回认证成功的
Authentication
对象。
先看前半部分查看用户是否存在
在这里调用了retrieveUser
方法来进行用户验证获取验证结果,这个方法在DaoAuthenticationProvider
中进行验证,是否存在该用户。
在DaoAuthenticationProvider
中调用loadUserByUsername
方法进行具体内容的验证,这个方法在前面UserDetailsService的作用 中看到过
验证密码是否正确
在完成用户存在验证后,我们继续看AbstractUserDetailsAuthenticationProvider
类,在这个类中使用additionalAuthenticationChecks
方法进行账号密码的验证。
具体内容的实现仍在在DaoAuthenticationProvider
中
5)请求拦截
上面我们可以知道UsernamePasswordAuthenticationFilter
拦截器,拦截的只是login的请求,那对于之后的每一次请求是个什么样的流程呢
通过拦截每一次请求,接着验证是否被授权,因此我们之后在处理请求拦截时,可以同样采用这样的方式,进行借鉴
6)汇总
发送登录请求 检查请求 是 否 默认 成功 失败 是 否 客户端 DispatcherServlet Spring Security Filter Chain SecurityContextHolder UsernamePasswordAuthenticationFilter 请求路径和方法匹配? 提取用户名和密码 继续过滤器链 创建 UsernamePasswordAuthenticationToken 调用 AuthenticationManager AuthenticationManager 委派给 AuthenticationProvider DaoAuthenticationProvider 调用 UserDetailsService UserDetailsService 加载 UserDetails 密码验证 返回 Authentication 对象 抛出 AuthenticationException 设置 SecurityContextHolder 认证成功处理 AuthenticationSuccessHandler 继续过滤器链 AuthenticationFailureHandler 重定向或返回成功响应 重定向到登录页面或显示错误 客户端
7)关于加密的过程
很多配置都是通过大致流程,因此可以扩展到理解其他的一些配置项。
我们发现在上面密码验证时,是设置了编码器,那我们从来没有配置过DaoAuthenticationProvider
,这里的密码加密器是怎么配置的呢?
在DaoAuthenticationProvider
构造方法设置加密器的位置添加断点。然后执行程序时不断进入断点。
进入了InitializeUserDetailsBeanManagerConfigurer
5. 结合数据库进行用户认证
数据库和上面配置过程不同的是:
手动配置
- 首先创建springSecurity的用户
- 在登录时对用户进行认证
- 与前面创建的用户进行匹配
数据库配置
- 不需要创建用户
- 登录时直接与数据库中的用户进行匹配
经过上面的过程,我们可以知道,主要的过程就是DaoAuthenticationProvider
创建时设置的UserDetailsService
,可以控制用户的认证。
1)引入数据库
我们采用springdatajpa操作数据库
向pom中添加
xml
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
设置yml内容
yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/springsecurity
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update #自动生成数据库
show-sql: true
1)创建实体类
创建好之后记得手动在数据库中添加一条数据用于测试
java
@Data
@Entity
@Table(name = "sys_user")
public class User {
@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int userId;
@Column(name = "mobile")
private String mobile;
@Column(name = "pwd")
private String password;
@Column(name="identity")
private int identity;
@Column(name="nick_name")
private String nickName;
}
2)创建Dao层
java
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
User findByMobileAndPassword(String mobile,String pwd);
User findByMobile(String mobile);
}
3)仿照InMemoryUserDetailsManager创建MyUserDetailsManager
我们上面知道了,要想控制账号密码的验证,我们就需要自己注入UserDetailsService
,这样他就不会采用系统本身的验证方案了。
java
@Component
public class MyUserDetailsManager implements UserDetailsService {
@Resource
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByMobile(username);
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(
user.getMobile(),
"{noop}"+user.getPassword(),// 这里{noop}前缀代表不进行加密,也就是匹配时与数据库中的明文相同即可
true,
true,
true,
true,
authorities
);
}
}
4)进行登录测试
6.漏洞保护
6.1 csrf跨域保护请求禁用
如果不禁用csrf,那么所有的post请求均会被拒绝
java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http
.csrf(csrf -> csrf.disable());
}
}
springsecurity实战应用
1. 构建项目
项目框架

配置文件
pom
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-jwt -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>5.8.27</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
yml
yml
server:
port: 11012
spring:
datasource:
url: jdbc:mysql://localhost:3306/springsecurity
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
实体类
java
@Data
@Entity
@Table(name = "sys_user")
public class User {
@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int userId;
@Column(name = "mobile")
private String mobile;
@Column(name = "pwd")
private String password;
@Column(name="identity")
private int identity;
@Column(name="nick_name")
private String nickName;
}
在执行项目之后,会自动构建数据库,在构建好数据库之后
记得手动插入一条数据
dao层
java
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
User findByMobileAndPassword(String mobile,String pwd);
User findByMobile(String mobile);
}
服务层
java
public interface UserService {
String login(String username,String password);
}
java
@Service
public class UserServiceImpl implements UserService {
UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public String login(String username, String password) {
User user = userDao.findByMobileAndPassword(username, password);
if (user!= null) {
return "login success"+user.getNickName();
} else {
return "login fail";
}
}
}
控制层
java
@RestController
@RequestMapping("/user")
public class UserController {
UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/test")
public String test() {
return "tt";
}
@GetMapping("/login")
public String login(@RequestParam(name = "account") String account, @RequestParam(name = "password") String password) {
return userService.login(account, password);
}
}
springSecurity
java
WebSecurityConfig
java
@Configuration
public class WebSecurityConfig {
//加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 授权管理器
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/user/login")
.authorizeHttpRequests(authorize -> authorize
.anyRequest().anonymous() // 允许匿名访问 /user/login
);
return http.build();
}
//Spring Security过滤链
@Bean
public SecurityFilterChain otherFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().hasRole("ADMIN")
)
.httpBasic(withDefaults());
return http.build();
}
}
Order的大小用于指明在第几层,越小越靠上,可以理解为优先级,越小越大
如果不设置order,那么会按照先后顺序进行配置
- 首先请求先通过order为1的过滤链,就是
/user/login
的请求,设置为允许匿名访问 - 而对于没有设置过滤链的请求,就会使用第二个配置
otherFilterChain
。这个配置被认为在apiFilterChain
之后,因为它的@Order
值在1
之后(没有@Order
默认为最后)
DBUserDetailsManager
继承了UserDetailsService,当加载用户的时候,就会执行这里的loadUserByUsername
java
@Component
public class DBUserDetailsManager implements UserDetailsService {
@Resource
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByMobile(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new MyUserDetail(user);
}
}
MyUserDetail
新建自己的UserDetails,继承原来的UserDetails,在里面添加我们自己定义的用户类,这样可以方便的存储我们自己的用户信息
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetail implements UserDetails {
private User user; // 这是自己定义的用户类
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getMobile();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2. 初步测试
在上面已经完成了接口的简单控制
我们可以通过访问localhost:11012/user/login?account=123&password=123
发现可以访问,并且登录成功
但是当我们访问localhost:11012/user/test
需要我们进行springsecurity的登录
BasicAuthenticationFilter