什么是 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>
让我们分解每个依赖项:
-
spring-boot-starter-security: 是 Spring Boot 框架的一部分,提供了一整套与安全相关的功能。
- 身份验证:帮助进行用户身份验证。
- 授权:支持基于角色和权限的访问控制。
- 开箱即用的常见安全配置。
- 与各种身份验证提供商集成。
-
aws-java-sdk-cognitoidp: 是 AWS SDK for Java 的一部分,专门关注 Amazon Cognito 身份池。
- 提供用于与 Amazon Cognito 身份池交互的 Java API。
- 使您的 Java 应用程序能够与 Amazon Cognito 集成以进行用户管理和身份验证。
- 包括用户注册、登录和其他身份相关操作的功能。
-
mysql-connector-java: 是 MySQL 数据库的官方 JDBC 驱动程序,允许 Java 应用程序与 MySQL 数据库连接并交互。因为您需要创建一个本地数据库。在线查找如何使用 IntelliJ 进行此操作。
-
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
在你的类中创建beanCognitoConfig
:
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) 直接注入方法参数并检查其是否有效。
我将让您在服务、请求、存储库和模型中实现逻辑。