如何在 Spring Boot 中使用 AWS Cognito 身份池 实现身份验证和授权

什么是 AWS Cognito

Amazon Cognito 是 Amazon Web Services (AWS) 的一款产品,用于控制移动应用程序的用户身份验证和访问。它是一个适用于网络和移动应用程序的身份平台。

Cognito 的主要功能包括:

  • User directory
  • 认证服务器
  • 授权服务
  • 用户注册和身份验证
  • 临时安全凭证
  • 会话管理
  • 忘记密码功能

Cognito 还提供了一个可以扩展到数百万用户的安全身份存储。此存储安全地存储直接注册的用户和使用外部身份提供商登录的联合用户的用户配置文件数据。

如何用Springboot实现

让我们构建一个像 pramp.com 这样的应用程序。

  • 用户可以注册并登录。
  • 管理员可以创建很多问题
  • 默认用户可以创建许多访谈会话
  • 每个面试环节有 2 个相同类型的问题

这是ERD以供更多理解

我假设您已经知道如何安装 Spring Boot。只需确保您的pom.xml文件中有以下依赖项

xml 复制代码
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-cognitoidp</artifactId>
            <version>1.11.934</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

让我们分解每个依赖项:

  1. spring-boot-starter-security: 是 Spring Boot 框架的一部分,提供了一整套与安全相关的功能。

    • 身份验证:帮助进行用户身份验证。
    • 授权:支持基于角色和权限的访问控制。
    • 开箱即用的常见安全配置。
    • 与各种身份验证提供商集成。
  2. aws-java-sdk-cognitoidp: 是 AWS SDK for Java 的一部分,专门关注 Amazon Cognito 身份池。

    • 提供用于与 Amazon Cognito 身份池交互的 Java API。
    • 使您的 Java 应用程序能够与 Amazon Cognito 集成以进行用户管理和身份验证。
    • 包括用户注册、登录和其他身份相关操作的功能。
  3. mysql-connector-java: 是 MySQL 数据库的官方 JDBC 驱动程序,允许 Java 应用程序与 MySQL 数据库连接并交互。因为您需要创建一个本地数据库。在线查找如何使用 IntelliJ 进行此操作。

  4. spring-boot-starter-oauth2-resource-server: 是 Spring Boot 的一部分,旨在设置 OAuth 2.0 资源服务器。

    • 将应用程序配置为充当资源服务器,能够处理和验证 OAuth 2.0 访问令牌。
    • 允许应用程序使用 OAuth 2.0 身份验证和授权来保护其资源和接口。

架构:

我们将使用领域驱动设计原则(DDD):

  • controller:用于处理 HTTP 请求并定义接口。
  • service:用于实现业务逻辑。存储库:用于与 DynamoDB 等数据库交互的数据访问。
  • model:用于定义领域实体和值对象。请求:我们可以使用数据传输对象(DTO)来封装层之间传递的数据。但对于这个项目,我们将使用请求类,它充当 DTO 的一种形式。这些类定义您期望在 HTTP 请求中接收的数据的结构。config:用于Spring配置类。security:用于与安全相关的类。

您的src/main文件夹应如下所示:

arduino 复制代码
├── java
│   └── com
│       └── example
│           └── chat_app
│               ├── ChatAppApplication.java
│               ├── config
│               │   ├── CognitoConfig.java
│               │   └── SecurityConfig.java
│               ├── controller
│               │   ├── QuestionController.java
│               │   └── UserController.java
│               ├── dto
│               ├── model
│               │   ├── Question.java
│               │   └── User.java
│               ├── repository
│               │   ├── QuestionRepository.java
│               │   └── UserRepository.java
│               ├── requests
│               │   ├── CreateQuestionRequest.java
│               │   ├── UserLoginRequest.java
│               │   └── UserRegistrationRequest.java
│               ├── security
│               └── service
│                   ├── CognitoService.java
│                   ├── QuestionService.java
│                   ├── QuestionServiceImpl.java
│                   ├── UserService.java
│                   └── UserServiceImpl.java
└── resources
    ├── application.properties
    ├── simple.priv
    ├── static
    └── templates

注册和登录

  • 创建 Cognito 用户池:查看本教程来执行此操作。
  • 将以下内容添加到您的 application.properties 文件中
ini 复制代码
spring.security.oauth2.client.registration.cognito.client-id=your_cognito_app_client_id
spring.security.oauth2.client.registration.cognito.client-secret=your_cognito_app_client_secret
spring.security.oauth2.client.registration.cognito.scope=openid
spring.security.oauth2.client.provider.cognito.issuer-uri=https://cognito-idp.us-east-2.amazonaws.com/us-east-2_EDjR6VYYS https://cognito-idp.<REGION>.amazonaws.com/<POOL Id>
aws.accessKeyId=aws_access_key
aws.secretKey=aws_secret_key
aws.region=aws_region
spring.datasource.url=database_uri
spring.datasource.username=database_username
spring.datasource.password=database_password
  • 在以下位置添加登录和注册路由的控制器操作UserController
java 复制代码
@Autowired
    private UserServiceImpl userService;

    @PostMapping(value = "/register", consumes = {"application/json"})
    public ResponseEntity<User> registerUser(@Valid @RequestBody UserRegistrationRequest userRegistrationRequest) {
        System.out.println(userRegistrationRequest);
        // Perform user registration
        User registeredUser = userService.registerUser(userRegistrationRequest);
        if (registeredUser != null) {
            return ResponseEntity.ok(registeredUser);
        } else {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }

    }

    @PostMapping("/login")
    public ResponseEntity<User> loginUser(@Valid @RequestBody UserLoginRequest userLoginRequest) {
        // Perform user login
        User loggedInUser = userService.loginUser(userLoginRequest.getUsername(), userLoginRequest.getPassword());

        if (loggedInUser != null) {
            // Successful login
            return ResponseEntity.ok(loggedInUser);
        } else {
            // Invalid login, return an appropriate response
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }
  • 在接口中添加registerUser和方法并在类中实现它们: loginUser``UserService``UserServiceImpl
java 复制代码
public interface UserService {
    User registerUser(UserRegistrationRequest userRegistrationRequest);
    User loginUser(String username, String password);
}
java 复制代码
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private CognitoService cognitoService;

    @Override
    public User registerUser(UserRegistrationRequest userRegistrationRequest) {
        String username = userRegistrationRequest.getUsername();
        String password = userRegistrationRequest.getPassword();
        String email = userRegistrationRequest.getEmail();
        // Register the user with Amazon Cognito
        return cognitoService.registerUser(username, email, password);
    }

    @Override
    public User loginUser(String username, String password) {
        return cognitoService.loginUser(username, password);
    }
}
  • 在CognitoService中实现注册和登录逻辑:
java 复制代码
@Value("${spring.security.oauth2.client.registration.cognito.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.cognito.client-secret}")
    private String clientSecret;

    @Value("${spring.security.oauth2.client.registration.cognito.scope}")
    private String scope;

    @Value("${spring.security.oauth2.client.provider.cognito.issuer-uri}")
    private String issuerUri;

    @Autowired
    private AWSCognitoIdentityProvider cognitoIdentityProvider;

    @Autowired
    private UserRepository userRepository;

    public User registerUser(String username, String email, String password) {
        // Set up the AWS Cognito registration request
        SignUpRequest signUpRequest = new SignUpRequest()
                .withClientId(clientId)
                .withUsername(username)
                .withPassword(password)
                .withUserAttributes(
                        new AttributeType().withName("email").withValue(email)
                );

        // Register the user with Amazon Cognito
        try {
            SignUpResult signUpResponse = cognitoIdentityProvider.signUp(signUpRequest);

            User registeredUser = new User();
            registeredUser.setUsername(username);
            registeredUser.setEmail(email);
            registeredUser.setPassword(password);

            return userRepository.save(registeredUser);

        } catch (Exception e) {
            throw new RuntimeException("User registration failed: " + e.getMessage(), e);
        }
    }

    public User loginUser(String username, String password) {
        // Set up the authentication request
        InitiateAuthRequest authRequest = new InitiateAuthRequest()
                .withAuthFlow("USER_PASSWORD_AUTH")
                .withClientId(clientId)
                .withAuthParameters(
                        Map.of(
                                "USERNAME", username,   // Use email as the username
                                "PASSWORD", password
                        )
                );

        try {
            InitiateAuthResult authResult = cognitoIdentityProvider.initiateAuth(authRequest);
            System.out.println(authResult);
            AuthenticationResultType authResponse = authResult.getAuthenticationResult();

            // At this point, the user is successfully authenticated, and you can access JWT tokens:
            String accessToken = authResponse.getAccessToken();
            String idToken = authResponse.getIdToken();
            String refreshToken = authResponse.getRefreshToken();

            // You can decode and verify the JWT tokens for user information

            User loggedInUser = new User();
            loggedInUser.setUsername(username);
            loggedInUser.setAccessToken(accessToken); // Store the token for future requests

            return loggedInUser;

        } catch (Exception e) {
            throw new RuntimeException("User login failed: " + e.getMessage(), e);
        }
    }
  • AWSCognitoIdentityProvider在你的类中创建bean CognitoConfig
java 复制代码
@Configuration
public class CognitoConfig {

    @Value("${aws.accessKeyId}")
    private String accessKey;

    @Value("${aws.secretKey}")
    private String secretKey;

    @Value("${aws.region}")
    private String region;


    @Bean
    public AWSCognitoIdentityProvider cognitoIdentityProvider() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

        return AWSCognitoIdentityProviderClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .withRegion(Regions.fromName(region))
                .build();
    }
}
  • 实现 UserLoginRequest 和 UserRegistrationRequest 类:
java 复制代码
public class UserLoginRequest {
    @NotBlank(message = "Username is required")
    private String username;

    @NotBlank(message = "Password is required")
    private String password;

    public String getUsername() {
        return username;
    }
    public String getPassword() {
        return password;
    }
}
java 复制代码
public class UserRegistrationRequest {

    @NotBlank(message = "Username is required")
    private String username;

    @NotBlank(message = "Password is required")
    private String password;

    @Email(message = "Invalid email address")
    private String email;

    // Getters and setters for the fields

    public String getUsername() {
        return username;
    }
    public String getPassword() {
        return password;
    }
    public String getEmail() {
        return email;
    }

}
  • 实现用户model:
java 复制代码
@Entity
@Table(
        name = "users",
        uniqueConstraints = {
                @UniqueConstraint(columnNames = "email"),
                @UniqueConstraint(columnNames = "username")
        }
)
public class User {
    @jakarta.persistence.Id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column
    @NotEmpty(message = "Username is required")
    @Size(min = 5, max = 50)
    private String username;

    @Column
    @NotEmpty(message = "Email is required")
    private String email;

    @Column
    @NotEmpty(message = "Password is required")
    private String password;

    @Transient
    private String accessToken;

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
  • 最后,将其添加到您的 SecurityConfig 类中:
java 复制代码
@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(
            requests -> requests
                    .requestMatchers("/admin")
                    .hasAnyRole("Admin", "Editor")
                    .requestMatchers("/login")
                    .permitAll()
                    .requestMatchers("/register")
                    .permitAll()
                    .requestMatchers("/logout")
                    .permitAll()
                    .anyRequest()
                    .authenticated()
        );

        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

现在启动您的 Spring 应用程序,打开 Postman,并确保您的应用程序已连接到数据库。运行POST /register请求,您应该得到以下结果:

json 复制代码
{
    "id": <user_id>,
    "username": <username>,
    "email": <email>,
    "password": <password>,
    "accessToken": null
}

您还应该能够在 AWS Cognito 应用程序中看到新用户。

运行POST /login请求,您应该得到以下结果:

json 复制代码
{
    "id": <user_id>,
    "username": <username>,
    "email": <email>,
    "password": null,
    "accessToken": <access_token>,
}

不要忘记从 AWS Cognito 手动确认用户帐户。您可以查阅文档,了解如何使用 Spring Boot 来完成此操作。

验证授权

Spring Security 支持使用两种形式的 OAuth 2.0 承载令牌来保护接口:

  • JWT

  • Opaque Tokens

    我们将使用 JWT。您可以在Spring 文档中了解更多相关信息。

    Spring Boot 将通过检查客户端(在本例中为 Postman)提供的令牌,验证它是否是有效的 Cognito 令牌,然后返回成功或未经授权的状态来完成此操作。

  • 更新你的Securityconfig

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;

    private final JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256;

    private final EncryptionMethod encryptionMethod = EncryptionMethod.A256GCM;

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    URL jwkSetUri;

    @Value("${sample.jwe-key-value}")
    RSAPrivateKey key;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(
            requests -> requests
                    .requestMatchers("/admin")
                    .hasAnyRole("Admin", "Editor")
                    .requestMatchers("/login")
                    .permitAll()
                    .requestMatchers("/register")
                    .permitAll()
                    .requestMatchers("/logout")
                    .permitAll()
                    .anyRequest()
                    .authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));

        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return new NimbusJwtDecoder(jwtProcessor());
    }

    private JWTProcessor<SecurityContext> jwtProcessor() {
        JWKSource<SecurityContext> jwsJwkSource = new RemoteJWKSet<>(this.jwkSetUri);
        JWSKeySelector<SecurityContext> jwsKeySelector = new JWSVerificationKeySelector<>(this.jwsAlgorithm,
                jwsJwkSource);

        JWKSource<SecurityContext> jweJwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey()));
        JWEKeySelector<SecurityContext> jweKeySelector = new JWEDecryptionKeySelector<>(this.jweAlgorithm,
                this.encryptionMethod, jweJwkSource);

        ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWEKeySelector(jweKeySelector);

        return jwtProcessor;
    }

    private RSAKey rsaKey() {
        RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key;
        Base64URL n = Base64URL.encode(crtKey.getModulus());
        Base64URL e = Base64URL.encode(crtKey.getPublicExponent());
        return new RSAKey.Builder(n, e).privateKey(this.key).keyUse(KeyUse.ENCRYPTION).build();
    }
}
  • 将此行添加到您的application.properties文件中:
arduino 复制代码
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://cognito-idp.<region>.amazonaws.com/<userPoolID>/.well-known/jwks.json
sample.jwe-key-value: classpath:simple.priv
  • 创建一个simple.priv文件并粘贴以下密钥。或者创建您自己的 RSA 密钥:
bash 复制代码
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA
iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM
g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK
LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF
oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc
3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn
+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE
E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek
lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG
mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7
62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0
bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA
+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH
Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA
8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd
I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY
QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d
rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk
HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA
Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN
HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a
FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF
snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H
c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM
TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR
47jndeyIaMTNETEmOnms+as17g==
-----END PRIVATE KEY-----

就是这样。现在假设您想创建一个问题。只需创建接口并在 QuestionController 中添加控制器操作:

java 复制代码
@Autowired
    private QuestionServiceImpl questionService;

    @PostMapping(value = "/questions", consumes = {"application/json"})
    public ResponseEntity<Question> createQuestion(@AuthenticationPrincipal Jwt jwt, @RequestBody CreateQuestionRequest request) {
        Question createdQuestion = questionService.createQuestion(jwt, request);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdQuestion);
    }

@AuthenticationPrincipal用于将 JSON Web Token (JWT) 直接注入方法参数并检查其是否有效。

我将让您在服务、请求、存储库和模型中实现逻辑。

相关推荐
louisgeek2 分钟前
Java 位运算
java
绝无仅有29 分钟前
企微审批对接错误与解决方案
后端·算法·架构
hweiyu0043 分钟前
Maven 私库
java·maven
Super Rookie1 小时前
Spring Boot 企业项目技术选型
java·spring boot·后端
来自宇宙的曹先生1 小时前
用 Spring Boot + Redis 实现哔哩哔哩弹幕系统(上篇博客改进版)
spring boot·redis·后端
写不出来就跑路1 小时前
Spring Security架构与实战全解析
java·spring·架构
expect7g1 小时前
Flink-Checkpoint-1.源码流程
后端·flink
00后程序员1 小时前
Fiddler中文版如何提升API调试效率:本地化优势与开发者实战体验汇总
后端
ZeroNews内网穿透2 小时前
服装零售企业跨区域运营难题破解方案
java·大数据·运维·服务器·数据库·tcp/ip·零售
用户8122199367222 小时前
C# .Net Core零基础从入门到精通实战教程全集【190课】
后端