第3章 Spring Security 的用户认证机制(2024 最新版)(上)+https://developer.aliyun.com/article/1487143
3.2.3 拓展案例 1:内存中的用户存储
对于开发环境或小型应用,可能你只需要一个简单的用户存储而不想引入外部数据库的复杂性。在这种情况下,Spring Security 允许你在内存中定义用户列表,这是一个快速启动和测试安全配置的好方法。
案例 Demo
让我们创建一个 Spring Boot 应用,其中用户认证信息存储在内存中,以简化认证流程。
步骤 1: 添加 Spring Security 依赖
首先,确保你的 pom.xml
包含了 Spring Security 的起步依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
步骤 2: 配置内存中的用户存储
创建一个安全配置类 SecurityConfig
,在这个类中,你将定义用户的认证信息。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("admin").roles("ADMIN"); } @SuppressWarnings("deprecation") @Bean public PasswordEncoder passwordEncoder() { // WARNING: This is only for demonstration purposes and should not be used in production return NoOpPasswordEncoder.getInstance(); } }
在这个配置中,我们定义了两个用户,一个是普通用户,另一个是管理员,都使用了不安全的密码编码器 NoOpPasswordEncoder
来避免密码加密。注意:NoOpPasswordEncoder
已被弃用,仅用于演示目的,生产环境中应使用强密码编码器如 BCryptPasswordEncoder
。
步骤 3: 测试认证
运行你的 Spring Boot 应用,并尝试访问任何受保护的资源。你将被重定向到登录页面,此时可以使用上面定义的用户凭证进行登录。
通过这个简单的案例,你可以快速地在 Spring Boot 应用中实现用户认证功能,而不需要配置外部数据源。这对于原型开发或小型应用来说是一个理想的选择。
3.2.4 拓展案例 2:使用 JdbcUserDetailsManager
当你的应用需要从关系型数据库中加载用户信息进行认证时,Spring Security 提供了一个方便的方式来实现这一需求——使用 JdbcUserDetailsManager
。这个方法不仅支持用户信息的加载,还支持通过 JDBC 进行用户的增加、删除和修改操作。
案例 Demo
我们将构建一个 Spring Boot 应用,使用 JdbcUserDetailsManager
从数据库加载用户信息进行认证。
步骤 1: 添加依赖
在你的 pom.xml
文件中添加 Spring Boot 的数据 JPA 起步依赖和数据库驱动依赖(以 H2 数据库为例)。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
步骤 2: 配置数据源和安全配置
在 application.properties
文件中配置你的数据库连接,以及启用 H2 控制台。
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.h2.console.enabled=true spring.sql.init.platform=h2
创建安全配置类 SecurityConfig
,配置 JdbcUserDetailsManager
。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.JdbcUserDetailsManager; import javax.sql.DataSource; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource()) .usersByUsernameQuery("select username, password, enabled from users where username=?") .authoritiesByUsernameQuery("select username, authority from authorities where username=?") .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); } }
步骤 3: 初始化用户数据
通常,你需要预先在数据库中创建 users
和 authorities
表,并插入一些测试用户。你可以使用 Spring SQL 初始化特性来自动完成这一步骤。
在 src/main/resources/schema.sql
中定义表结构:
CREATE TABLE users ( username VARCHAR(50) NOT NULL PRIMARY KEY, password VARCHAR(100) NOT NULL, enabled BOOLEAN NOT NULL ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) );
在 src/main/resources/data.sql
中插入测试数据:
INSERT INTO users (username, password, enabled) VALUES ('user', '{bcrypt}加密后的密码', true); INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
确保将 {bcrypt}加密后的密码
替换为使用 BCryptPasswordEncoder
加密的密码。
步骤 4: 测试认证
启动应用并访问任何受保护的 URL,系统应重定向你到登录页面。使用步骤 3 中定义的用户凭证进行登录,验证认证流程是否按预期工作。
通过使用 JdbcUserDetailsManager
,你可以轻松地将 Spring Security 与数据库用户存储集成,实现复杂的认证和用户管理功能。这种方式适合需要持久化用户信息的中大型应用。
3.3 自定义认证逻辑
在 Spring Security 中,定制认证逻辑可以让你实现更加复杂和安全的认证机制,比如添加验证码校验、实现多因素认证等。Spring Security 提供了灵活的扩展点,使得定制认证过程变得可行。
3.3.1 基础知识详解
认证管理器 (AuthenticationManager
)
AuthenticationManager
是 Spring Security 认证机制的中心接口,负责协调认证过程。- 它定义了一个
authenticate(Authentication authentication)
方法,用于尝试对输入的认证信息进行认证。
认证提供者 (AuthenticationProvider
)
AuthenticationProvider
接口定义了实际执行认证逻辑的方法。- 每个
AuthenticationProvider
都能处理特定类型的认证请求。例如,你可以有一个专门处理用户名和密码认证的提供者,另一个处理基于短信验证码的认证。
用户详细信息服务 (UserDetailsService
)
UserDetailsService
是一个用于加载用户特定数据的核心接口。它只有一个方法loadUserByUsername(String username)
,用于根据用户名查找UserDetails
对象。UserDetails
对象包含了一些基本的用户信息,如用户名、密码、权限等。
密码编码 (PasswordEncoder
)
- 在认证过程中,用户提交的密码需要与存储在系统中的密码进行比对。为了安全起见,存储的密码通常是加密过的。
PasswordEncoder
接口定义了加密和匹配用户密码的方法。使用强加密算法(如 BCrypt)可以有效防止密码被破解。
自定义认证逻辑的意义
- 安全性提升:通过自定义认证逻辑,开发者可以引入额外的安全措施,如验证码验证、多因素认证等,进一步增强应用的安全性。
- 灵活性和扩展性:每个应用的安全需求都是独特的。Spring Security 的可扩展性允许开发者根据自己的需求定制认证过程。
- 集成第三方服务:在现代的应用中,常常需要集成第三方认证服务(如社交登录)。自定义认证逻辑使得这种集成变得可行。
通过深入理解和应用上述基础知识,开发者可以充分利用 Spring Security 提供的强大功能,实现既安全又符合业务需求的认证机制。
3.3.2 主要案例:添加验证码校验
在这个案例中,我们将实现一个包含验证码校验的自定义认证流程,以增强登录过程的安全性。验证码校验是一种常见的防止自动化攻击(如暴力破解、机器人登录)的手段。
案例 Demo
假设我们的 Spring Boot 应用有一个登录表单,我们想在用户提交表单时验证一个验证码字段。
步骤 1: 集成验证码服务
首先,假设你有一个服务来生成和验证验证码,我们将其称为 CaptchaService
。
@Service public class CaptchaService { public boolean validateCaptcha(String sessionId, String captcha) { // 模拟验证码校验逻辑 // 在实际应用中,应该根据 sessionId 查找预先存储的验证码,并与用户输入的 captcha 进行比对 return "expectedCaptcha".equals(captcha); } }
步骤 2: 自定义认证过滤器
创建一个自定义的认证过滤器 CustomAuthenticationFilter
,在尝试认证之前校验验证码。
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Autowired private CaptchaService captchaService; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String captcha = request.getParameter("captcha"); String sessionId = request.getSession().getId(); if (!captchaService.validateCaptcha(sessionId, captcha)) { throw new AuthenticationServiceException("验证码错误"); } return super.attemptAuthentication(request, response); } }
步骤 3: 配置自定义过滤器
在你的安全配置类中,注册自定义的认证过滤器并设置其在 UsernamePasswordAuthenticationFilter
之前执行。
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationFilter customAuthenticationFilter; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // 配置用户认证逻辑,例如使用 inMemoryAuthentication 或其他 } @Override protected void configure(HttpSecurity http) throws Exception { http // 其他配置... .addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); return filter; } }
步骤 4: 测试验证码功能
启动应用并访问登录页面。尝试不输入验证码或输入错误的验证码提交登录表单,应该被阻止并提示验证码错误。
通过实现这个案例,你将学会如何在 Spring Security 的认证过程中加入额外的校验步骤,如验证码校验,以增强应用的安全性。这种方法为防止自动化攻击提供了一层额外的保护。
3.3.3 拓展案例 1:实现基于短信的认证
在这个案例中,我们将通过实现基于短信验证码的认证流程来增强应用的安全性。这种方法适合于需要额外安全措施的场景,例如金融应用或敏感数据的访问控制。
案例 Demo
步骤 1: 创建短信验证码服务
首先,创建一个服务来发送和验证短信验证码,这里我们称之为 SmsCodeService
。
@Service public class SmsCodeService { private final Map<String, String> smsCodeStorage = new ConcurrentHashMap<>(); public void sendSmsCode(String mobile, String code) { // 实际项目中应调用短信服务提供商的API发送短信 // 这里仅做模拟 smsCodeStorage.put(mobile, code); System.out.println("Sending SMS code " + code + " to " + mobile); } public boolean validateSmsCode(String mobile, String code) { String validCode = smsCodeStorage.get(mobile); return code.equals(validCode); } }
步骤 2: 定义短信认证令牌
创建一个 SmsAuthenticationToken
类来表示短信认证的令牌。
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private String code; public SmsAuthenticationToken(String mobile, String code) { super(null); this.principal = mobile; this.code = code; setAuthenticated(false); } public SmsAuthenticationToken(String mobile, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = mobile; this.code = null; super.setAuthenticated(true); // must use super, as we override } @Override public Object getCredentials() { return code; } @Override public Object getPrincipal() { return this.principal; } }
步骤 3: 实现短信认证提供者
创建一个 SmsAuthenticationProvider
类来实现短信验证码的认证逻辑。
@Component public class SmsAuthenticationProvider implements AuthenticationProvider { @Autowired private SmsCodeService smsCodeService; @Autowired private MyUserDetailsService myUserDetailsService; // 自定义的UserDetailsService实现 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); String code = (String) authenticationToken.getCredentials(); if (!smsCodeService.validateSmsCode(mobile, code)) { throw new BadCredentialsException("短信验证码错误"); } UserDetails userDetails = myUserDetailsService.loadUserByMobile(mobile); if (userDetails == null) { throw new UsernameNotFoundException("用户不存在"); } SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return (SmsAuthenticationToken.class.isAssignableFrom(authentication)); } }
步骤 4: 配置短信认证
在安全配置类中注册 SmsAuthenticationProvider
。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SmsAuthenticationProvider smsAuthenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(smsAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { // 配置安全规则 } }
步骤 5: 测试短信认证
启动应用并尝试使用短信验证码登录。你可以创建一个简单的登录页面,用于输入手机号和验证码,然后通过后端接口验证。
通过实现这个案例,你将能够为你的应用添加基于短信验证码的二次认证流程,提高安全性。这种认证方式在需要额外验证用户身份的场景中非常有用,尤其是在移动应用中。
3.3.4 拓展案例 2:集成第三方认证服务
在这个案例中,我们将演示如何将第三方认证服务(例如 OAuth2 提供商,如 Google 或 Facebook)集成到 Spring Boot 应用中,为用户提供更多便捷的登录选项。
案例 Demo
假设我们希望用户可以通过 Google 账户登录我们的应用。
步骤 1: 添加依赖
首先,确保你的 pom.xml
包含了 Spring Security OAuth2 客户端的起步依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
步骤 2: 配置 OAuth2 客户端
在 application.yml
或 application.properties
文件中配置 OAuth2 客户端信息。以下是使用 Google 作为 OAuth2 提供商的一个例子:
spring.security.oauth2.client.registration.google.client-id=your-google-client-id spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret spring.security.oauth2.client.registration.google.scope=profile,email
你需要替换 your-google-client-id
和 your-google-client-secret
为你在 Google Cloud Platform 创建 OAuth2 客户端时获得的客户端 ID 和密钥。
步骤 3: 创建安全配置
创建一个安全配置类 SecurityConfig
,配置使用 OAuth2 登录:
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); } }
这个配置确保了所有请求都需要认证,并启用了 OAuth2 登录。
步骤 4: 测试 OAuth2 登录
启动应用并尝试访问任何受保护的资源。系统应该会重定向你到 Google 的登录页面。使用你的 Google 账户登录后,你将被重定向回应用,并成功访问受保护的资源。
拓展:自定义 OAuth2 登录行为
Spring Security 允许你自定义 OAuth2 登录行为,例如自定义登录成功或失败后的处理逻辑:
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login() .userInfoEndpoint() .oidcUserService(this.oidcUserService()); } private OidcUserService oidcUserService() { OidcUserService service = new OidcUserService(); service.setUserinfoCustomizer((userRequest, user) -> { // 自定义用户信息处理逻辑 }); return service; } }
通过实现这个案例,你可以向你的应用添加更多的登录选项,提供更便捷的用户体验,并利用第三方服务提供的安全性。这种方法尤其适用于希望简化登录过程或提供社交登录选项的现代应用。