使用 Spring Boot 3 实现 Spring Security 6:使用 Nimbus 进行 OAuth 和 JWT 身份验证的指南
介绍
自从 Spring Security 6 推出以来,我遇到过很多开发人员,他们在设置它来满足他们的业务需求时遇到了问题。
因此如果
- 您对 Spring Security 还不熟悉
- 或者您一直在使用旧版本的 Spring Security 和 Spring Boot,并且发现使用 Spring Security 6 在 Spring Boot 3 上实现 Spring Security 很困难。
- 您正在寻找一种更简单的方法来设置 Spring Security,这样您就不必为 JWT 安装外部库并创建完整的过滤器。
那么本文适合您。
首先,让我们深入了解 Spring Security 的基础知识以及使用 Nimbus for JWT 设置 Spring Security 所需的内容。
什么是 Spring Security
根据 Spring 文档的定义,Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是 Spring 应用程序安全的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。
Spring Security 的一些功能包括
- 对身份验证和授权的全面且可扩展的支持
- 防范会话固定、点击劫持、跨站请求伪造等攻击
- Servlet API 集成
- 可选与 Spring Web MVC 集成
- 更多内容…
使用 Spring 安全性非常重要,因为它包含更新的安全功能,从而确保您的应用程序具有最新的安全功能。
项目设置
好了,理论讲完了,不用动手了。我们直接开始搭建项目吧。
现在,如果你对 Spring 稍有了解,你应该知道每个 Spring 开发者的专属网站 spring-initializer。
我们将添加以下依赖项
所以我们添加了
- 用于构建 Web API 的 Spring Web
- OAuth2 资源服务器确保安全
- Spring data JPA,因为我们将利用存储来存储用户数据
- PostgreSQL 驱动程序,因为我们将使用 Postgres 数据库。
注意:我们没有使用 Spring Security,而是使用了 OAuth2 Resource Server。这是因为我发现 OAuth2 Resource Server 包含 Nimbus,它可以用来生成和管理 JWT,而无需添加额外的依赖项。另请注意,我使用的是 Spring Boot 3.1.4。
现在,一旦我们完成了依赖项的设置,我们就可以下载 jar 文件,提取我们的项目,然后在我们选择的任何 IDE 中打开它(在我的情况下,我选择了 Intellij)。
设置应用程序配置
现在我们需要设置我们的应用程序配置,并设置与我们的Postgres数据库的连接(如果您使用 Postgres,但如果您决定使用像 H2 这样的内存数据库,您可以在线查看如何连接,尽管您仍然必须根据您的喜好在 application.yaml 或 application.properties 中执行此操作)。
spring:
application:
name: spring-security-jwt
datasource:
url: jdbc:postgresql://localhost:5432/spring-security
username:
password:
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
server:
port: 8000
error:
include-message: always
rsa:
private-key: classpath:certs/private-key.pem
public-key: classpath:certs/public-key.pem
logging:
level:
org:
springframework: INFO
创建用于加密和解密的公钥和私钥
现在在资源目录中,我们创建一个名为 certs 的文件夹,然后打开终端并导航到该目录并运行此命令
cd src/main/resources/certs
然后我们将使用 OpenSSL 生成 RSA 密钥对(这应该是 Mac 用户的默认设置,也可以为其他用户设置)
生成私钥(RSA):
openssl genpkey -algorithm RSA -out private-key.pem
此命令生成一个 RSA 私钥并将其保存到 private-key.pem 文件中。
通过运行以下命令从私钥中提取公钥:
openssl rsa -pubout -in private-key.pem -out public-key.pem
然后将其转换为适当的 PCKS 格式并替换旧格式
openssl pkcs8 -topk8 -inform PEM -outform PEM -in private-key.pem -out private-key.pem -nocrypt
好吧,如果最后一步一切顺利,我们继续下一步。
回想一下,在 application.yaml 文件中有一段如下所示的配置。
rsa:
private-key: classpath:certs/private-key.pem
public-key: classpath:certs/public-key.pem
我们所做的就是告诉 Spring 在哪里可以找到用于加密和解密我们的 JWT 令牌的公钥和私钥。
设置我们的用户模块并在应用程序中使用 RSAkeys
现在我们已经使用 OpenSSL 创建了 RSAkeys,我们现在要做的是通过配置属性帮助 spring-boot 使用它。
- 首先,我们需要创建一个名为config 的包,然后在我们之前创建的 config 包中创建一个文件,并将其命名为RsaKeyConfigProperties,然后将下面的代码粘贴到其中
package com.tutorial.springsecurityjwt.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyConfigProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey ) {
}
- 现在我们将创建用户模块来管理所有用户活动,然后我们将创建作为实体的User.java类。
package com.tutorial.springsecurityjwt.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.micrometer.common.lang.NonNull;
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = "user_name"),
@UniqueConstraint(columnNames = "email")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(unique = true)
private String userId;
@Column(name = "user_name", unique = true)
@NonNull
private String username;
@NonNull
@Column(name = "email", unique = true)
private String email;
@NonNull
@JsonIgnore
private String password;
public User() {
}
public User(String userId, @NonNull String username, @NonNull String email, @NonNull String password) {
this.userId = userId;
this.username = username;
this.email = email;
this.password = password;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
@NonNull
public String getUsername() {
return username;
}
public void setUsername(@NonNull String username) {
this.username = username;
}
@NonNull
public String getEmail() {
return email;
}
public void setEmail(@NonNull String email) {
this.email = email;
}
@NonNull
public String getPassword() {
return password;
}
public void setPassword(@NonNull String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", username='" + username + '\'' +
", email='" + email + '\'' +
", password='" + password + '\'' +
'}';
}
}
- 然后,我们将创建UserRepository接口来处理 JPA 数据库交互和查询,它将扩展 JPARepository。粘贴下面的代码。
package com.tutorial.springsecurityjwt.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByUsername(String username);
}
设置我们的授权模块
- 让我们设置用于管理用户权限和角色的 AuthUser。将以下代码粘贴到 auth 模块中的 AuthUser.java 类中。
package com.tutorial.springsecurityjwt.auth;
import com.tutorial.springsecurityjwt.user.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
public class AuthUser extends User implements UserDetails {
private final User user;
public AuthUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 我们将创建 AuthServices.java 类,它将管理所有启用身份验证的逻辑。粘贴以下代码
package com.tutorial.springsecurityjwt.auth;
import com.tutorial.springsecurityjwt.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.stream.Collectors;
@Service
public class AuthService {
private static final Logger log = LoggerFactory.getLogger(AuthService.class);
@Autowired
private JwtEncoder jwtEncoder;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
public String generateToken(Authentication authentication) {
Instant now = Instant.now();
String scope = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(10, ChronoUnit.HOURS))
.subject(authentication.getName())
.claim("scope", scope)
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
- 我们还将在 auth 包JpaUserDetailsService.java中创建一个类,用于从数据库加载用户进行登录。
package com.tutorial.springsecurityjwt.auth;
import com.tutorial.springsecurityjwt.user.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
@Service
public class JpaUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AuthUser user = userRepository
.findByUsername(username)
.map(AuthUser::new)
.orElseThrow(() -> new UsernameNotFoundException("User name not found: " + username));
return user;
}
}
设置我们的 Spring Security 配置
在我们的配置包中创建一个名为SecurityConfig.java的 java 类并将以下代码粘贴到里面。
package com.tutorial.springsecurityjwt.config;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.tutorial.springsecurityjwt.auth.JpaUserDetailsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
private RsaKeyConfigProperties rsaKeyConfigProperties;
@Autowired
private JpaUserDetailsService userDetailsService;
@Bean
public AuthenticationManager authManager() {
var authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(authProvider);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
return http
.csrf(csrf -> {
csrf.disable();
})
.cors(cors -> cors.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/error/**").permitAll();
auth.requestMatchers("/api/auth/**").permitAll();
auth.anyRequest().authenticated();
})
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer((oauth2) -> oauth2.jwt((jwt) -> jwt.decoder(jwtDecoder())))
.userDetailsService(userDetailsService)
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeyConfigProperties.publicKey()).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeyConfigProperties.publicKey()).privateKey(rsaKeyConfigProperties.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
注意:您必须将这行代码添加 到您的基础应用程序(在我的例子中是 SpringSecurityJwtApplication.java)中。否则,您的应用程序将无法获取您的配置。@EnableConfigurationProperties(RsaKeyConfigProperties.class)
您的基本应用程序应该看起来像这样。
package com.tutorial.springsecurityjwt;
import com.tutorial.springsecurityjwt.config.RsaKeyConfigProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@EnableConfigurationProperties(RsaKeyConfigProperties.class)
@SpringBootApplication
public class CollaboMainApplication {
public static void main(String[] args) {
SpringApplication.run(CollaboMainApplication.class, args);
}
}
设置控制器
现在我们需要设置我们的 auth rest 控制器,使其包含登录路由,并在实际应用中包含注册路由等。
但在本例中,我们只需设置一个登录路由,并在数据库中创建一个硬编码的用户。
为此,我们必须首先创建AuthDTO(DTO 表示数据传输对象),以便接收登录用户名和密码。然后在 auth 模块/包中设置AuthController.java,并将以下代码粘贴到其中。
package com.tutorial.springsecurityjwt.auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
@Validated
public class AuthController {
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthService authService;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthDTO.LoginRequest userLogin) throws IllegalAccessException {
Authentication authentication =
authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(
userLogin.username(),
userLogin.password()));
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthUser userDetails = (AuthUser) authentication.getPrincipal();
log.info("Token requested for user :{}", authentication.getAuthorities());
String token = authService.generateToken(authentication);
AuthDTO.Response response = new AuthDTO.Response("User logged in successfully", token);
return ResponseEntity.ok(response);
}
}
- 现在,我们已经完成了使用 Nimbus 设置 Spring Security 和 JWT 的基本步骤,接下来需要进行测试,并在数据库中创建虚拟用户。我们将在应用程序启动时使用命令行运行器创建用户。因此,我们将在SpringSecurityJwtApplication.java类中创建一个 bean,作为应用程序的入口点。您可以使用以下代码更新入口类。
package com.tutorial.springsecurityjwt;
import com.tutorial.springsecurityjwt.config.RsaKeyConfigProperties;
import com.tutorial.springsecurityjwt.user.User;
import com.tutorial.springsecurityjwt.user.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableConfigurationProperties(RsaKeyConfigProperties.class)
@SpringBootApplication
public class SpringSecurityJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityJwtApplication.class, args);
}
@Bean
public CommandLineRunner initializeUser(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
return args -> {
User user = new User();
user.setUsername("exampleuser");
user.setEmail("example@gmail.com");
user.setPassword(passwordEncoder.encode("examplepassword"));
// Save the user to the database
userRepository.save(user);
};
}
}
测试我们的登录和 JWT
现在我们可以开始了,您要做的就是运行您的应用程序并确保遵循所有程序,当您的应用程序启动时没有任何错误,您可以在 Postman 上测试您的登录端点。
概括
简要解释我们的设置的所有组件和身份验证流程
- 我们使用 springdataJPA、OAuth2 资源服务器、postgres 驱动程序和 spring web 建立了我们的项目。
- 我们创建了用户包、授权包和配置包。
- 我们使用 OpenSSL 创建公钥和私钥来加密和解密 JWT,并通过 applicationConfig.yaml 将其链接到我们的应用程序并设置属性。
- 我们创建了一个用户实体和一个用户存储库,用于进行 JPA 数据库调用。
- 我们创建了一个可以管理角色、凭证等的授权用户。
- 我们创建了一个 JpaUserDetailsService 来管理登录时的用户详细信息,authService 来管理生成令牌等身份验证逻辑。
- 我们创建了一个 DTO 来帮助我们管理客户端和服务器请求和响应之间的数据传输。
- 我们创建了一个 authController 来管理登录等身份验证请求的路由。
- 然后,我们设置安全配置以使用 Nimbus 来管理 JWT,并使用我们的 UserDetailsService 来管理登录时的用户详细信息。
下面是文件结构的图片
如果您遇到任何问题,可以随时参考 GitHub 仓库。以下是包含项目代码的 GitHub 仓库链接。
如果您有任何问题,可以给我发送邮件
siruchechukwuisaac@gmail.com 。
我真的希望这篇文章对您有所帮助。
谢谢。
文章来源:https://dev.to/pryhmez/implementing-spring-security-6-with-spring-boot-3-a-guide-to-oauth-and-jwt-with-nimbus-for-authentication-2lhf