Sping Security + OAuth2 在Spring 3.x中的使用
Sping Security + OAuth2 在Spring 3.x中的使用
Authentication(身份认证)依赖引入
- JDK:17
- SpringBoot:3.x (Spring Security 6.x.0) | Spring Cloud 2022
- 其他依赖:Spring Web,Spring Security
spring cloud自从2020.0.0(含)以上版本就移除了spring-cloud-security-dependencies依赖,所以从2020.0.0版本开始,无法引入spring-cloud-starter-oauth2。
而oauth2的授权服务分离为一个独立的项目。我们要在项目中添加 spring-security-oauth2-authorization-server依赖
|
|
我用的是springboot 3.0.2 , spring cloud 2022
Spring Security 底层原理
Spring Security 默认行为
- 保护应用程序URL,要求对应用程序的任何交互进行身份验证
- 所有应用程序URL都必须经过身份验证才能访问。
- 程序启动时生成一个默认用户“user”
- 在程序启动时生成一个默认用户“user”。
- 生成一个默认的随机密码,并将此密码记录在控制台上。
- 生成默认的登录表单和注销页面
- 提供基于表单的登录和注销流程。
- 对于Web请求
- 未登录用户的Web请求重定向到登录页面。
- 对于服务请求
- 未经授权的服务请求返回401状态码。
- 处理跨站请求伪造(CSRF)攻击
- 实施CSRF保护措施,确保请求的合法性。
- 处理会话劫持攻击
- 实施会话保护措施,防止会话劫持。
- 写入Strict-Transport-Security以确保HTTPS
- 设置
Strict-Transport-Security
头部,强制使用HTTPS。
- 设置
- 写入X-Content-Type-Options以处理探测攻击
- 设置
X-Content-Type-Options
头部,防止MIME类型探测。
- 设置
- 写入Cache Control头来保护经过身份验证的资源
- 设置
Cache-Control
头部,防止缓存敏感信息。
- 设置
- 写入X-Frame-Options以处理点击劫持攻击
- 设置
X-Frame-Options
头部,防止点击劫持攻击。
- 设置
Spring Security过滤器流程
-
FilterChain
Servlet容器创建了一个
FilterChain
,其中包含Filter
实例和Servlet
.根据请求URI的路径处理HttpServletRequest
多个
Filter
在FilterChain的作用:-
阻止
HttpServletRequest
被下游Filter
或Servlet
调用,Filter
将写入HttpServletResponse
。 -
修改下游
Filter
实例和Servlet
实例使用的HttpServletRequest
或HttpServletResponse
。
-
由于
Filter
只影响下游Filter
实例和Servlet
,因此调用每个Filter
的顺序非常重要。
在Spring MVC应用程序中,
Servlet
是DispatcherServlet
的一个实例。 一个Servlet
可以处理单个HttpServletRequest
和HttpServletResponse
-
DelegatingFilterProxy
Spring提供了一个名为
DelegatingFilterProxy
的Filter
实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext
之间架桥。 Servlet容器允许使用自己的标准注册Filter
实例,但它不知道spring定义的bean。 您可以通过标准的Servlet容器机制注册DelegatingFilterProxy
,但将所有工作委托给实现Filter
的Spring Bean。DelegatingFilterProxy
是一个代理类,DelegatingFilterProxy通过标准的Servlet容器机制注册到FilterChain。因为Servlet并不知道Spring的Bean,DelegatingFilterProxy负责将Spring容器的Filter
定义的bean注册到FilterChain的DelegatingFilterProxy
。 -
FilterChainProxy
Spring Security的Servlet支持包含在
FilterChainProxy
中。FilterChainProxy
是Spring Security提供的一个特殊的Filter
,它允许通过SecurityFilterChain
委托给多个Filter
实例。 因为FilterChainProxy
是一个Bean,所以它通常被封装在DelegatingFilterProxy中。 -
SecurityFilterChain
SecurityFilterChain
中的安全过滤器通常是bean,但是它们注册在FilterChainProxy
而不是DelegatingFilterProxy。FilterChainProxy
为直接向Servlet容器或DelegatingFilterProxy注册提供了许多优点。 它为Spring Security的所有Servlet支持提供了一个起点。FilterChainProxy
决定应该使用哪个SecurityFilterChain
。 只调用第一个匹配的SecurityFilterChain
。 如果请求/api/messages/
的URL,它首先匹配/api/**
的SecurityFilterChain0
模式,因此只调用SecurityFilterChain0
,即使它也匹配SecurityFilterChainn
。 如果请求URL/messages/
,它与/api/**
的SecurityFilterChain0
模式不匹配,因此FilterChainProxy
继续尝试每个SecurityFilterChain
。 -
Security Filters
使用SecurityFilterChain API将安全过滤器插入到FilterChainProxy中。 这些过滤器可用于许多不同的目的,如身份验证、授权、漏洞利用保护等。 过滤器按照特定的顺序执行,以确保在正确的时间调用它们,例如,执行身份验证的
Filter
应该在执行授权的Filter
之前调用。 通常不需要知道Spring Security的Filter
s的顺序。 然而,有时知道顺序是有益的,如果你想知道它们,你可以检查FilterOrderRegistration
代码。
DefaultSecurityFilterChain默认过滤器链
DefaultSecurityFilterChain属于SecurityFilterChain。是Spring默认实现的安全过滤器链,里面有16个默认实现的过滤器实例Bean。
过滤器列表在应用程序启动时以INFO级别打印,因此可以在控制台输出中看到如下内容:
|
|
-
DisableEncodeUrlFilter
- 禁用URL编码,以确保安全性。
-
WebAsyncManagerIntegrationFilter
- 支持Spring异步请求处理,与Spring Security集成。
-
SecurityContextHolderFilter
- 管理
SecurityContext
,在请求生命周期中持有和清理SecurityContext
。
- 管理
-
HeaderWriterFilter
- 写入安全头部信息(如HSTS、X-Content-Type-Options、X-Frame-Options等)。
-
CsrfFilter
- 防止跨站请求伪造(CSRF)攻击,通过检查CSRF令牌的合法性来保护应用程序。
-
CorsFilter
- 处理跨域资源共享(CORS)请求,允许或拒绝来自不同域的请求,根据配置的CORS策略来决定请求是否被允许。
CorsFilter必须要打开http.cors()允许跨域才会添加进DefaultSecurityFilterChain
1 2 3 4 5
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(); return http.build(); }
-
LogoutFilter
- 处理注销请求,清理用户会话并重定向到注销页面。
-
UsernamePasswordAuthenticationFilter
- 处理基于用户名和密码的表单登录请求。
-
DefaultLoginPageGeneratingFilter
- 生成默认的登录页面,用于未提供自定义登录页面时。
-
DefaultLogoutPageGeneratingFilter
- 生成默认的注销页面,用于未提供自定义注销页面时。
-
BasicAuthenticationFilter
- 处理HTTP Basic认证。
-
RequestCacheAwareFilter
- 管理缓存的请求,以便在成功认证后重定向到用户最初请求的URL。
-
SecurityContextHolderAwareRequestFilter
- 在Spring MVC中提供基于安全上下文的便利方法,允许安全相关信息在控制器中使用。
-
AnonymousAuthenticationFilter
- 为未认证用户提供匿名身份,以便安全过滤器链能够继续处理。
-
ExceptionTranslationFilter
- 处理和翻译安全异常,将其转换为合适的HTTP响应(如403 Forbidden)。
-
AuthorizationFilter
- 执行授权决策,确保用户具有访问请求资源的权限。
Spring Security 自定义配置
-
SecurityConfig自定义配置类
-
1 2 3 4 5 6
//@EnableWebSecurity //开启SpringSecurity的自定义配置(spirngboot项目可以省略) @Configuration public class SecurityConfig { }
@EnableWebSecurity在SpringBoot项目中可以忽略
spirng-boot-autoconfigure包里面自动配置了。只要添加了Spring Security依赖,EnableWebSecurity.clss自动在SpringBoot上下文中配置。@EnableWebSecurity注解也会自动加入到配置类中。
SpringSecurity的默认配置
SpringSecurity默认实现了HttpSecurity安全配置
|
|
.authorizeHttpRequests()
负责组装用户认证过程的具体功能.anyRequest().authenticated()
所有请求开启授权保护,未认证跳转登录页,已认证的请求会自动授权。.formLogin(withDefaults())
默认表单登录登出页面- 如果将.formLogin(withDefaults())注释掉可以查看浏览器的默认登录框
.httpBasic()
。
- 如果将.formLogin(withDefaults())注释掉可以查看浏览器的默认登录框
.httpBasic(withDefaults())
使用默认基本授权方式- 由浏览器提供默认的登录输入框,不同浏览器不同
- 如果注释掉,Spring启动时不会加载BasicAuthenticationFilter过滤器
UserDetailsService类与默认实现
在Spring Security中,UserDetailsService
接口用于从特定的数据源加载用户信息。实现这个接口的类可以有很多种,具体实现类取决于你如何存储和管理用户数据。下面是一些常见的UserDetailsService
实现类:
-
InMemoryUserDetailsManager:
- 用于在内存中存储用户信息,适用于小型应用程序或测试场景。
1 2 3 4 5 6 7 8 9 10 11
@Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser( User.withDefaultPasswordEncoder() //使用默认的无编码密码 NoOpPasswordEncoder .withUsername("user") .password("password") .roles("USER") //用户角色 .build()); return manager; }
- 创建基于内存的用户信息管理器
- 使用manager管理UserDetails
-
JdbcUserDetailsManager:
- 使用JDBC从关系型数据库中加载用户信息。
1 2 3 4 5 6 7
@Bean public UserDetailsService userDetailsService(DataSource dataSource) { JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource); userDetailsManager.setUsersByUsernameQuery("select username, password, enabled from users where username = ?"); userDetailsManager.setAuthoritiesByUsernameQuery("select username, authority from authorities where username = ?"); return userDetailsManager; }
-
LdapUserDetailsManager:
- 使用LDAP从LDAP服务器加载用户信息。
1 2 3 4 5 6 7 8
@Bean public LdapUserDetailsManager ldapUserDetailsManager(BaseLdapPathContextSource contextSource) { LdapUserDetailsManager manager = new LdapUserDetailsManager(); manager.setContextSource(contextSource); manager.setUserDnPatterns("uid={0},ou=people"); manager.setGroupSearchBase("ou=groups"); return manager; }
-
CustomUserDetailsService:
- 自定义实现
UserDetailsService
,可以从任意数据源加载用户信息,例如NoSQL数据库、Web服务等。
- 自定义实现
|
|
基于内存的用户验证的流程
认证流程
-
程序启动时
- 创建
InMemoryUserDetailsManager
对象。 - 创建
User
对象,封装用户名和密码。 - 使用
InMemoryUserDetailsManager
将User
存入内存。
- 创建
-
校验用户时
-
Spring Security 自动使用
InMemoryUserDetailsManager
的loadUserByUsername
方法从内存中获取User
对象。 -
在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中,将用户输入的用户名和密码与从内存中获取到的用户信息进行比较,进行用户认证。
|
|
流程分析
-
检查请求方法:
1 2 3
if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); }
- 如果
postOnly
为true
且请求方法不是POST
,则抛出AuthenticationServiceException
异常,表示不支持非POST
方法的认证请求。
- 如果
-
获取用户名和密码:
1 2 3 4
String username = this.obtainUsername(request); username = username != null ? username.trim() : ""; String password = this.obtainPassword(request); password = password != null ? password : "";
- 调用
obtainUsername(request)
获取请求中的用户名,如果用户名不为null
,则去掉前后空格,否则设置为空字符串。 - 调用
obtainPassword(request)
获取请求中的密码,如果密码不为null
,则使用该密码,否则设置为空字符串。
- 调用
-
创建未认证的
UsernamePasswordAuthenticationToken
对象:1
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
- 使用获取到的用户名和密码封装一个未认证的
UsernamePasswordAuthenticationToken
对象。
在第5步的时候,Token用来验证
authentication
对象是否UsernamePasswordAuthenticationToken
类的实例1 2 3 4 5 6 7
Assert.isInstanceOf( UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported" ); });
- 使用获取到的用户名和密码封装一个未认证的
-
设置认证请求的详细信息:
1
this.setDetails(request, authRequest);
- 调用
setDetails(request, authRequest)
方法,将请求详细信息(如请求的remoteAddress
和sessionId
等)设置到authRequest
中。
- 调用
-
进行认证:
1
return this.getAuthenticationManager().authenticate(authRequest);
- 调用
getAuthenticationManager().authenticate(authRequest)
方法进行认证。返回一个包含认证信息的Authentication
对象。如果认证失败,会抛出相应的异常。
- 调用
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { UsernameNotFoundException ex = var6; this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; }throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { AuthenticationException ex = var7; if (!cacheWasUsed) { throw ex; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks( user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user);
-
12行
retrieveUser
函数对用户输入的用户名和密码与内存中的用户名和密码进行比对 -
loadUserByUsername
这个函数很重要,是用户认证的核心函数。1
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
-
35行
additionalAuthenticationChecks
函数1 2 3 4 5 6 7 8 9 10 11 12 13
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
if (authentication.getCredentials() == null)
判断用户凭证- 在用户名密码模式下,用户凭证是用户名和密码
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
使用对应的密码编码器对用户名和密码进行匹配。
基于数据库的用户验证流程
参照基于内存的用户验证,我们想要用数据库提供的用户数据进行用户验证。我们需要自己实现一个UserDetailsService
的Bean。想要提供UserDetailsService
首先要实现一个基于数据库的DBUserDetailService
|
|
可以看到InMemoryUserDetailsManager
实现了两个接口UserDetailsManager
和UserDetailsPasswordService
|
|
|
|
可以看到UserDetailsManager
继承了UserDetailsService
,
|
|
Spring Security 自动使用
InMemoryUserDetailsManager
的loadUserByUsername
方法从内存中获取User
对象。
所以参照基于内存的用户验证,我们可以这样做⬇️
认证流程
-
程序启动时
- 创建 自定义的
DBUserDetailsManager
对象。实现两个接口UserDetailsManager
和UserDetailsPasswordService
。
2.创建User
对象,封装用户名和密码。3. 使用InMemoryUserDetailsManager
将User
存入内存。 - 创建 自定义的
-
校验用户时
- Spring Security 自动使用
DBUserDetailsManager
的loadUserByUsername
方法从数据库中获取User
对象。 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中,将用户输入的用户名和密码与从数据库中获取到的用户信息进行比较,进行用户认证。
- Spring Security 自动使用
自己实现UserDetailService 认证
- 实现
UserDetailsManager
UserDetailsPasswordService
接口的函数。
|
|
- 📌实现重要的函数:
loadUserByUsername(String username)
|
|
如果不设置密码加密编码器,可以使用无加密编码器
NoOpPasswordEncoder
1 2 3 4
@Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
⚠️实现UserDetailsService踩坑
报错:
|
|
如果你实现了自定义的UserDetailService
,可以在配置类使用@Bean
注解创建了UserDetailsService Bean。或者在实现的UserDetailService
类上注解@Component
,也会自动创建Bean到SpringBoot容器。
🚫 千万不要 重复创建 UserDetailsService Bean。
如果想要不同的UserDetailsService实现方法。可以定义一个自己的
Authenticationprovider
。