在本文中,我们将探索 Spring 安全性,并使用 OAuth 2.0 构建一个身份验证系统。
在深入了解 Spring Security 的运行方式之前,了解基于 Java 的 Web 服务器中的请求处理生命周期至关重要。 Spring Security 无缝集成到此生命周期中以保护传入请求。
使用 Spring Security 在基于 Spring 的应用程序中处理 HTTP 请求的生命周期涉及多个阶段,每个阶段在处理、验证和保护请求方面都发挥着关键作用。
当客户端(例如浏览器、移动应用程序或 Postman 等 API 工具)向服务器发送 HTTP 请求时,生命周期就开始了。
示例:
GET /api/admin/dashboard HTTP/1.1
Servlet 容器(例如 Tomcat)接收请求并将其委托给 DispatcherServlet(Spring 应用程序中的前端控制器)。这是应用程序处理管道开始的地方。
在 DispatcherServlet 处理请求之前,Spring Security 的过滤器链 会拦截它。过滤器链是一系列过滤器,每个过滤器负责处理特定的安全任务。这些过滤器确保请求在到达应用程序逻辑之前满足身份验证和授权要求。
身份验证过滤器:
这些过滤器验证请求是否包含有效的凭据,例如用户名/密码、JWT 或会话 cookie。
授权过滤器:
身份验证后,这些过滤器确保经过身份验证的用户具有访问所请求资源所需的角色或权限。
其他滤镜:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
如果身份验证成功,Spring Security 将创建一个 Authentication 对象并将其存储在 SecurityContext 中。该对象通常存储在线程本地存储中,在整个请求生命周期中都可以访问。
Principal:代表经过身份验证的用户(例如用户名)。
凭据:包括 JWT 令牌或密码等身份验证详细信息。
权限:包含分配给用户的角色和权限。
请求通过身份验证过滤器。
如果凭据有效,则会创建 Authentication 对象并将其添加到 SecurityContext。
如果凭据无效,ExceptionTranslationFilter 会向客户端发送 401 未经授权的响应。
一旦请求成功通过 Spring Security 过滤器链,DispatcherServlet 就会接管:
处理程序映射:
它根据 URL 和 HTTP 方法将传入请求映射到适当的控制器方法。
控制器调用:
映射的控制器通常在服务和存储库等其他 Spring 组件的帮助下处理请求并返回适当的响应。
Spring Security 通过其过滤器将自身集成到此生命周期中,在最早阶段拦截请求。当请求到达应用程序逻辑时,它已经经过身份验证和授权,确保核心应用程序仅处理合法流量。
Spring Security 的设计确保以声明方式处理身份验证、授权和其他安全措施,使开发人员能够根据需要灵活地自定义或扩展其行为。它不仅强制执行最佳实践,还简化了现代应用程序中复杂安全要求的实现。
探索了 Spring Security 中的过滤器链之后,让我们深入研究一些在身份验证和授权过程中发挥关键作用的其他关键组件。
AuthenticationManager 是一个定义单个方法的接口,authenticate(Authenticationauthentication),用于验证用户的凭据并确定它们是否有效。您可以将 AuthenticationManager 视为一个协调器,您可以在其中注册多个提供商,并且根据请求类型,它将向正确的提供商发送身份验证请求。
AuthenticationProvider 是一个接口,它定义了一个合约,用于根据用户的凭据对用户进行身份验证。它代表特定的身份验证机制,例如用户名/密码、OAuth 或 LDAP。多个 AuthenticationProvider 实现可以共存,允许应用程序支持各种身份验证策略。
身份验证对象:
AuthenticationProvider 处理 Authentication 对象,该对象封装了用户的凭据(例如用户名和密码)。
验证方法:
每个 AuthenticationProvider 都实现了authenticate(Authenticationauthentication)方法,实际的身份验证逻辑驻留在其中。此方法:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
数据库支持的 AuthenticationProvider 验证用户名和密码。
基于 OAuth 的 AuthenticationProvider 验证外部身份提供商颁发的令牌。
UserDetailsService 在 Spring 文档中被描述为加载用户特定数据的核心接口,它包含一个方法 loadUserByUsername ,该方法接受用户名作为参数并返回 ==User== 身份对象。基本上我们创建并实现了 UserDetailsService 的类,在其中重写了 loadUserByUsername 方法。
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
现在这三者如何协同工作的是 AuthenticationManager 会要求 AuthenticationProvider 根据指定的 Provider 类型进行身份验证,而 UserDetailsService 实现将帮助 AuthenticationProvider 证明用户详细信息。
用户使用其凭据(用户名和密码)或 JWT 令牌(标头中)向经过身份验证的端点发送请求,并将请求传递到身份验证过滤器
AuthenticationFilter(例如,UsernamePasswordAuthenticationFilter):
这个自定义过滤器扩展了 OncePerRequestFilter 并放置在 UsernamePasswordAuthenticationFilter 之前,它的作用是从请求中提取令牌并验证它。
如果令牌有效,它会创建一个 UsernamePasswordAuthenticationToken 并将该令牌设置到安全上下文中,告诉 Spring Security 该请求已通过身份验证,并且当此请求传递到 UsernamePasswordAuthenticationFilter 时,它只是传递,因为它具有 UsernamePasswordAuthenticationToken
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
如果我们在 UserDetails 类的帮助下验证用户名和密码后传递了用户名和密码而不是令牌,则此 UsernamePasswordAuthenticationToken 是在 AuthenticationManager 和 AuthenticationProvider 的帮助下生成的。
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
UserDetailsService:AuthenticationProvider 使用 UserDetailsService 根据用户名加载用户详细信息。我们为此提供了 UserDetailsService
凭证验证:它将提供的密码与存储在用户详细信息中的密码进行比较(通常使用密码编码器)。
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
现在需要配置所有这些不同的过滤器和 bean,以便 Spring security 知道要做什么,因此我们创建一个配置类,在其中指定所有配置。
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
到目前为止,我们已经在 Spring Security 的帮助下理解并配置了我们的身份验证,现在是时候测试它了。
我们将创建一个简单的应用程序,其中包含两个控制器 AuthController(处理登录和注册)和 ProductController(虚拟受保护控制器)
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{ return config.getAuthenticationManager(); }
@Bean public AuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsServiceImpl); authenticationProvider.setPasswordEncoder(passwordEncoder); return authenticationProvider; }
到目前为止,我们已经实现了注册、登录和验证,但是如果我还想添加 Login With Google/Github 功能,那么我们可以借助 OAuth2.0 来实现
OAuth 2.0 是一种用于授权的协议,通过它,用户可以授予第三方应用程序访问存储在其他平台(例如 Google Drive、Github)上的资源的权限,而无需共享这些平台的凭据。
它主要用于启用社交登录,例如“使用 google 登录”、“使用 github 登录”。
Google、Facebook、Github 等平台提供了授权服务器,该服务器实现了 OAuth 2.0 协议,用于此社交登录或授权访问。
现在我们将一一研究每个概念
资源所有者是想要授权第三方应用程序(您的应用程序)的用户。
这是您的(第三方)应用程序想要从资源服务器访问数据或资源。
它是存储用户数据的服务器,供第三方应用程序访问。
对资源所有者进行身份验证并向客户端(例如 Google 帐户)颁发访问令牌的服务器。
授权服务器向客户端颁发的凭证,允许客户端代表用户访问资源服务器。它的生命周期通常很短,很快就会过期,因此还提供了刷新令牌来刷新此访问令牌,以便用户不需要再次授权。
用户授予的特定权限,定义客户端可以和不能对用户数据执行什么操作。例如,对于授权,我们只需要用户信息,如个人资料、姓名等,但对于文件访问,需要不同的范围。
客户端应用程序从授权服务器获取访问令牌的方法。授权定义了授权客户端应用程序访问资源所有者的受保护数据的流程和条件。
它是安全的,因为我们不需要向浏览器公开我们的客户端密钥和其他凭据
OAuth 2.0 提供了两种最常用的 Grant 类型
它是最常用的授权/方法类型,最安全,并且适用于服务器端应用程序
在此,授权码由客户端提供给后端,后端将访问令牌提供给客户端。
流程:
由单页应用程序 (SPA) 或没有后端的应用程序使用。在此,访问令牌是在浏览器本身中直接生成和颁发的。
流程:
为了完全理解,我们将分别实现两者,但首先我们将实现我们需要的授权代码授予
授权服务器
它可以是任何一个平台(例如 google 、 github),或者您也可以使用 KeyCloak 创建自己的平台,或者也可以遵循 OAuth 2.0 标准构建您自己的平台(我们可能会在下一篇博客中这样做?)
Spring Boot 应用
这将是我们的主要后端应用程序/服务,它将处理代码交换、验证、保存用户详细信息和分配 JWT 令牌等所有操作
React 应用程序(前端)
这将是我们的客户端,它将用户重定向到授权服务器进行授权。
因此,在我们的实现中,我们要做的是前端(网络/应用程序)将我们的用户重定向到谷歌登录,并将 uri 重定向到我们的后端端点,这将进一步控制我们稍后将讨论它以及redirect_url我们还传递应用程序的客户端 ID,所有这些都将在查询参数中发送。
否,当用户成功登录谷歌时,授权服务器(谷歌的)会将我们的请求重定向到后端端点,我们要做的就是与授权服务器交换授权代码以获取访问令牌和刷新令牌,然后我们可以根据需要处理身份验证,最后我们会将响应发送回我们的前端,该前端将有一个 cookie 并重定向到我们的仪表板,或者可能是受保护的页面。
现在我们将研究代码,但请确保在 OAuth 客户端的 Google 控制台仪表板中的授权重定向 URL 中添加后端端点的 URL。
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
就是这样,它可以正常工作,为了测试,您可以制作一个简单的前端应用程序,它除了具有上下文之外什么都没有,而您不知道登录和注册功能。
感谢您阅读这么久,如果您有任何建议,请在评论中提出
以上是了解 Spring Security 和 OAuth的详细内容。更多信息请关注PHP中文网其他相关文章!