Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
J
java-jwt
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Phùng Quốc Toàn
java-jwt
Commits
58a1b9d6
Commit
58a1b9d6
authored
Apr 25, 2025
by
Phùng Quốc Toàn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add password management features including change, reset, and forgot password functionality
parent
09bbd610
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
458 additions
and
150 deletions
+458
-150
pom.xml
pom.xml
+127
-123
CustomJwtDecoder.java
...teria/identityservice/configuration/CustomJwtDecoder.java
+15
-17
SecurityConfig.java
...evteria/identityservice/configuration/SecurityConfig.java
+1
-1
AuthenticationController.java
.../identityservice/controller/AuthenticationController.java
+25
-0
UserController.java
...m/devteria/identityservice/controller/UserController.java
+1
-1
ChangePasswordRequest.java
...ia/identityservice/dto/request/ChangePasswordRequest.java
+14
-0
ForgotPasswordRequest.java
...ia/identityservice/dto/request/ForgotPasswordRequest.java
+13
-0
ResetPasswordRequest.java
...ria/identityservice/dto/request/ResetPasswordRequest.java
+15
-0
UserCreationRequest.java
...eria/identityservice/dto/request/UserCreationRequest.java
+7
-0
User.java
src/main/java/com/devteria/identityservice/entity/User.java
+3
-0
OtpType.java
...main/java/com/devteria/identityservice/enums/OtpType.java
+17
-0
ErrorCode.java
...ava/com/devteria/identityservice/exception/ErrorCode.java
+3
-1
UserRepository.java
...m/devteria/identityservice/repository/UserRepository.java
+5
-0
AuthenticationService.java
...vteria/identityservice/service/AuthenticationService.java
+46
-6
EmailService.java
...va/com/devteria/identityservice/service/EmailService.java
+89
-0
OtpService.java
...java/com/devteria/identityservice/service/OtpService.java
+58
-0
UserService.java
...ava/com/devteria/identityservice/service/UserService.java
+2
-1
application.yaml
src/main/resources/application.yaml
+17
-0
No files found.
pom.xml
View file @
58a1b9d6
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>
4.0.0
</modelVersion>
<parent>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-parent
</artifactId>
<version>
3.2.2
</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<groupId>
com.devteria
</groupId>
<artifactId>
identity-service
</artifactId>
<name>
identity-service
</name>
<version>
0.0.1-SNAPSHOT
</version>
<description>
Identity service
</description>
<properties>
<java.version>
21
</java.version>
<projectlombok-lombok.version>
1.18.30
</projectlombok-lombok.version>
<mapstruct.version>
1.5.5.Final
</mapstruct.version>
<lombok-mapstruct-binding.version>
0.2.0
</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-jpa
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-validation
</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto -->
<dependency>
<groupId>
org.springframework.security
</groupId>
<artifactId>
spring-security-crypto
</artifactId>
</dependency>
<dependency>
<groupId>
com.mysql
</groupId>
<artifactId>
mysql-connector-j
</artifactId>
<scope>
runtime
</scope>
</dependency>
<dependency>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<version>
${projectlombok-lombok.version}
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
org.mapstruct
</groupId>
<artifactId>
mapstruct
</artifactId>
<version>
${mapstruct.version}
</version>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-test
</artifactId>
<scope>
test
</scope>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-oauth2-resource-server
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-actuator
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-cache
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-redis
</artifactId>
</dependency>
</dependencies>
<project
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns=
"http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>
4.0.0
</modelVersion>
<parent>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-parent
</artifactId>
<version>
3.2.2
</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<groupId>
com.devteria
</groupId>
<artifactId>
identity-service
</artifactId>
<name>
identity-service
</name>
<version>
0.0.1-SNAPSHOT
</version>
<description>
Identity service
</description>
<properties>
<java.version>
21
</java.version>
<projectlombok-lombok.version>
1.18.30
</projectlombok-lombok.version>
<mapstruct.version>
1.5.5.Final
</mapstruct.version>
<lombok-mapstruct-binding.version>
0.2.0
</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-jpa
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-validation
</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto -->
<dependency>
<groupId>
org.springframework.security
</groupId>
<artifactId>
spring-security-crypto
</artifactId>
</dependency>
<dependency>
<groupId>
com.mysql
</groupId>
<artifactId>
mysql-connector-j
</artifactId>
<scope>
runtime
</scope>
</dependency>
<dependency>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<version>
${projectlombok-lombok.version}
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
org.mapstruct
</groupId>
<artifactId>
mapstruct
</artifactId>
<version>
${mapstruct.version}
</version>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-test
</artifactId>
<scope>
test
</scope>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-oauth2-resource-server
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-actuator
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-cache
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-redis
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-mail
</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-maven-plugin
</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>
org.apache.maven.plugins
</groupId>
<artifactId>
maven-compiler-plugin
</artifactId>
<version>
${maven-compiler-plugin.version}
</version>
<configuration>
<source>
${java.version}
</source>
<target>
${java.version}
</target>
<annotationProcessorPaths>
<path>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<version>
${projectlombok-lombok.version}
</version>
</path>
<path>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok-mapstruct-binding
</artifactId>
<version>
${lombok-mapstruct-binding.version}
</version>
</path>
<path>
<groupId>
org.mapstruct
</groupId>
<artifactId>
mapstruct-processor
</artifactId>
<version>
${mapstruct.version}
</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
-Amapstruct.suppressGeneratorTimestamp=true
</arg>
<arg>
-Amapstruct.defaultComponentModel=spring
</arg>
<arg>
-Amapstruct.verbose=true
</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<build>
<plugins>
<plugin>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-maven-plugin
</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>
org.apache.maven.plugins
</groupId>
<artifactId>
maven-compiler-plugin
</artifactId>
<version>
${maven-compiler-plugin.version}
</version>
<configuration>
<source>
${java.version}
</source>
<target>
${java.version}
</target>
<annotationProcessorPaths>
<path>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<version>
${projectlombok-lombok.version}
</version>
</path>
<path>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok-mapstruct-binding
</artifactId>
<version>
${lombok-mapstruct-binding.version}
</version>
</path>
<path>
<groupId>
org.mapstruct
</groupId>
<artifactId>
mapstruct-processor
</artifactId>
<version>
${mapstruct.version}
</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
-Amapstruct.suppressGeneratorTimestamp=true
</arg>
<arg>
-Amapstruct.defaultComponentModel=spring
</arg>
<arg>
-Amapstruct.verbose=true
</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/devteria/identityservice/configuration/CustomJwtDecoder.java
View file @
58a1b9d6
...
...
@@ -3,7 +3,8 @@ package com.devteria.identityservice.configuration;
import
com.devteria.identityservice.dto.request.IntrospectRequest
;
import
com.devteria.identityservice.service.AuthenticationService
;
import
com.nimbusds.jose.JOSEException
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
jakarta.annotation.PostConstruct
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.security.oauth2.jose.jws.MacAlgorithm
;
import
org.springframework.security.oauth2.jwt.Jwt
;
...
...
@@ -14,21 +15,27 @@ import org.springframework.stereotype.Component;
import
javax.crypto.spec.SecretKeySpec
;
import
java.text.ParseException
;
import
java.util.Objects
;
@Component
@RequiredArgsConstructor
public
class
CustomJwtDecoder
implements
JwtDecoder
{
private
final
AuthenticationService
authenticationService
;
@Value
(
"${jwt.signer-key}"
)
private
String
signerKey
;
@Autowired
private
AuthenticationService
authenticationService
;
private
NimbusJwtDecoder
nimbusJwtDecoder
=
null
;
private
NimbusJwtDecoder
nimbusJwtDecoder
;
@PostConstruct
public
void
initDecoder
()
{
SecretKeySpec
secretKeySpec
=
new
SecretKeySpec
(
signerKey
.
getBytes
(),
"HmacSHA512"
);
nimbusJwtDecoder
=
NimbusJwtDecoder
.
withSecretKey
(
secretKeySpec
)
.
macAlgorithm
(
MacAlgorithm
.
HS512
)
.
build
();
}
@Override
public
Jwt
decode
(
String
token
)
throws
JwtException
{
try
{
var
response
=
authenticationService
.
introspect
(
IntrospectRequest
.
builder
()
.
token
(
token
)
...
...
@@ -40,14 +47,6 @@ public class CustomJwtDecoder implements JwtDecoder {
throw
new
JwtException
(
e
.
getMessage
());
}
if
(
Objects
.
isNull
(
nimbusJwtDecoder
))
{
SecretKeySpec
secretKeySpec
=
new
SecretKeySpec
(
signerKey
.
getBytes
(),
"HS512"
);
nimbusJwtDecoder
=
NimbusJwtDecoder
.
withSecretKey
(
secretKeySpec
)
.
macAlgorithm
(
MacAlgorithm
.
HS512
)
.
build
();
}
return
nimbusJwtDecoder
.
decode
(
token
);
}
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/configuration/SecurityConfig.java
View file @
58a1b9d6
...
...
@@ -20,7 +20,7 @@ import org.springframework.security.web.SecurityFilterChain;
@RequiredArgsConstructor
public
class
SecurityConfig
{
private
final
String
[]
PUBLIC_ENDPOINTS
=
new
String
[]{
"/auth/token"
,
"/auth/introspect"
,
"/users"
,
"/auth/logout"
,
"/auth/refresh-token"
};
private
final
String
[]
PUBLIC_ENDPOINTS
=
new
String
[]{
"/auth/token"
,
"/auth/introspect"
,
"/users"
,
"/auth/logout"
,
"/auth/refresh-token"
,
"/auth/forgot-password"
,
"/auth/reset-password"
};
CustomJwtDecoder
customJwtDecoder
;
...
...
src/main/java/com/devteria/identityservice/controller/AuthenticationController.java
View file @
58a1b9d6
...
...
@@ -5,6 +5,7 @@ import com.devteria.identityservice.dto.response.AuthenticationResponse;
import
com.devteria.identityservice.dto.response.IntrospectResponse
;
import
com.devteria.identityservice.service.AuthenticationService
;
import
com.nimbusds.jose.JOSEException
;
import
jakarta.mail.MessagingException
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
...
...
@@ -54,4 +55,28 @@ public class AuthenticationController {
.
result
(
"Logout successful"
)
.
build
();
}
@PostMapping
(
"/change-password"
)
ApiResponse
<
String
>
changePassword
(
@RequestBody
ChangePasswordRequest
request
)
{
authenticationService
.
changePassword
(
request
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Password changed successfully"
)
.
build
();
}
@PostMapping
(
"/forgot-password"
)
ApiResponse
<
String
>
forgotPassword
(
@RequestBody
ForgotPasswordRequest
request
)
throws
MessagingException
{
authenticationService
.
forgotPassword
(
request
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Otp sent to your email"
)
.
build
();
}
@PostMapping
(
"/reset-password"
)
ApiResponse
<
String
>
resetPassword
(
@RequestBody
ResetPasswordRequest
request
)
{
authenticationService
.
resetPassword
(
request
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Password reset successfully"
)
.
build
();
}
}
src/main/java/com/devteria/identityservice/controller/UserController.java
View file @
58a1b9d6
...
...
@@ -34,7 +34,7 @@ public class UserController {
ApiResponse
<
List
<
UserResponse
>>
getUsers
()
{
var
authentication
=
SecurityContextHolder
.
getContext
().
getAuthentication
();
log
.
info
(
"User :{}"
,
authentication
.
getName
());
log
.
info
(
"User
id
:{}"
,
authentication
.
getName
());
authentication
.
getAuthorities
().
forEach
(
grantedAuthority
->
log
.
info
(
grantedAuthority
.
getAuthority
()));
return
ApiResponse
.<
List
<
UserResponse
>>
builder
()
...
...
src/main/java/com/devteria/identityservice/dto/request/ChangePasswordRequest.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
ChangePasswordRequest
{
String
oldPassword
;
String
newPassword
;
}
src/main/java/com/devteria/identityservice/dto/request/ForgotPasswordRequest.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
ForgotPasswordRequest
{
String
email
;
}
src/main/java/com/devteria/identityservice/dto/request/ResetPasswordRequest.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
ResetPasswordRequest
{
String
email
;
String
newPassword
;
String
otp
;
}
src/main/java/com/devteria/identityservice/dto/request/UserCreationRequest.java
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
jakarta.validation.constraints.Email
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.Size
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
...
...
@@ -15,8 +17,13 @@ public class UserCreationRequest {
@Size
(
min
=
3
,
message
=
"USERNAME_INVALID"
)
String
username
;
@Email
@NotBlank
String
email
;
@Size
(
min
=
8
,
message
=
"INVALID_PASSWORD"
)
String
password
;
String
firstName
;
String
lastName
;
...
...
src/main/java/com/devteria/identityservice/entity/User.java
View file @
58a1b9d6
...
...
@@ -19,6 +19,9 @@ public class User {
@GeneratedValue
(
strategy
=
GenerationType
.
UUID
)
String
id
;
String
username
;
String
email
;
String
password
;
String
firstName
;
String
lastName
;
...
...
src/main/java/com/devteria/identityservice/enums/OtpType.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
enums
;
import
lombok.AccessLevel
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
lombok.experimental.FieldDefaults
;
@Getter
@AllArgsConstructor
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
public
enum
OtpType
{
RESET_PASSWORD
(
"reset-password:"
,
2
*
60
),
// 2 phút
REGISTRATION
(
"registration:"
,
5
*
60
);
// 5 phút
String
name
;
int
expireTimeInSeconds
;
}
src/main/java/com/devteria/identityservice/exception/ErrorCode.java
View file @
58a1b9d6
...
...
@@ -15,7 +15,9 @@ public enum ErrorCode {
UNAUTHENTICATED
(
1006
,
"Unauthenticated"
,
HttpStatus
.
UNAUTHORIZED
),
UNAUTHORIZED
(
1007
,
"You do not have permission"
,
HttpStatus
.
FORBIDDEN
),
INVALID_DOB
(
1008
,
"Date of birth must be at least {min} years old"
,
HttpStatus
.
BAD_REQUEST
),
;
PASSWORD_NOT_MATCH
(
1009
,
"Password not match"
,
HttpStatus
.
BAD_REQUEST
),
INVALID_OTP
(
1010
,
"Mã OTP không hợp lệ hoặc đã hết hạn"
,
HttpStatus
.
BAD_REQUEST
),
EMAIL_SENDING_FAILED
(
1011
,
"Không thể gửi email"
,
HttpStatus
.
INTERNAL_SERVER_ERROR
);;
private
int
code
;
...
...
src/main/java/com/devteria/identityservice/repository/UserRepository.java
View file @
58a1b9d6
...
...
@@ -9,5 +9,10 @@ import java.util.Optional;
@Repository
public
interface
UserRepository
extends
JpaRepository
<
User
,
String
>
{
boolean
existsByUsername
(
String
username
);
Optional
<
User
>
findByUsername
(
String
username
);
boolean
existsByEmail
(
String
email
);
Optional
<
User
>
findByEmail
(
String
email
);
}
src/main/java/com/devteria/identityservice/service/AuthenticationService.java
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
service
;
import
com.devteria.identityservice.dto.request.AuthenticationRequest
;
import
com.devteria.identityservice.dto.request.IntrospectRequest
;
import
com.devteria.identityservice.dto.request.LogoutRequest
;
import
com.devteria.identityservice.dto.request.RefreshTokenRequest
;
import
com.devteria.identityservice.dto.request.*
;
import
com.devteria.identityservice.dto.response.AuthenticationResponse
;
import
com.devteria.identityservice.dto.response.IntrospectResponse
;
import
com.devteria.identityservice.entity.InvalidatedToken
;
import
com.devteria.identityservice.entity.User
;
import
com.devteria.identityservice.enums.OtpType
;
import
com.devteria.identityservice.exception.AppException
;
import
com.devteria.identityservice.exception.ErrorCode
;
import
com.devteria.identityservice.repository.UserRepository
;
...
...
@@ -16,6 +14,7 @@ import com.nimbusds.jose.crypto.MACSigner;
import
com.nimbusds.jose.crypto.MACVerifier
;
import
com.nimbusds.jwt.JWTClaimsSet
;
import
com.nimbusds.jwt.SignedJWT
;
import
jakarta.mail.MessagingException
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
...
...
@@ -23,6 +22,7 @@ import lombok.experimental.NonFinal;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.data.redis.core.RedisTemplate
;
import
org.springframework.security.core.context.SecurityContextHolder
;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.stereotype.Service
;
...
...
@@ -41,8 +41,13 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
public
class
AuthenticationService
{
private
static
final
String
TOKEN_KEY_PREFIX
=
"invalid_token:"
;
private
static
String
TOKEN_KEY_PREFIX
=
"invalid_token:"
;
RedisTemplate
<
String
,
Object
>
redisTemplate
;
PasswordEncoder
passwordEncoder
;
OtpService
otpService
;
EmailService
emailService
;
UserRepository
userRepository
;
@NonFinal
...
...
@@ -95,7 +100,7 @@ public class AuthenticationService {
JWSHeader
header
=
new
JWSHeader
(
JWSAlgorithm
.
HS512
);
JWTClaimsSet
jwtClaimsSet
=
new
JWTClaimsSet
.
Builder
()
.
subject
(
user
.
get
Username
())
.
subject
(
user
.
get
Id
())
.
issuer
(
"demo_jwt"
)
.
issueTime
(
new
Date
())
.
expirationTime
(
new
Date
(
...
...
@@ -217,4 +222,39 @@ public class AuthenticationService {
private
boolean
existsInvalidatedToken
(
String
tokenId
)
{
return
Boolean
.
TRUE
.
equals
(
redisTemplate
.
hasKey
(
TOKEN_KEY_PREFIX
+
tokenId
));
}
public
void
changePassword
(
ChangePasswordRequest
request
)
{
String
name
=
SecurityContextHolder
.
getContext
().
getAuthentication
().
getName
();
User
user
=
userRepository
.
findById
(
name
).
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
));
if
(!
passwordEncoder
.
matches
(
request
.
getOldPassword
(),
user
.
getPassword
()))
{
throw
new
AppException
(
ErrorCode
.
PASSWORD_NOT_MATCH
);
}
user
.
setPassword
(
passwordEncoder
.
encode
(
request
.
getNewPassword
()));
userRepository
.
save
(
user
);
}
public
void
resetPassword
(
ResetPasswordRequest
request
)
{
if
(!(
otpService
.
validateOtp
(
request
.
getOtp
(),
request
.
getEmail
(),
OtpType
.
RESET_PASSWORD
)))
{
throw
new
AppException
(
ErrorCode
.
INVALID_OTP
);
}
User
user
=
userRepository
.
findByEmail
(
request
.
getEmail
())
.
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
));
user
.
setPassword
(
passwordEncoder
.
encode
(
request
.
getNewPassword
()));
userRepository
.
save
(
user
);
}
public
void
forgotPassword
(
ForgotPasswordRequest
request
)
throws
MessagingException
{
if
(!(
userRepository
.
existsByEmail
(
request
.
getEmail
())))
{
throw
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
);
}
String
otp
=
otpService
.
generateAndSaveOtp
(
request
.
getEmail
(),
OtpType
.
RESET_PASSWORD
);
emailService
.
sendOtpEmail
(
request
.
getEmail
(),
otp
,
OtpType
.
RESET_PASSWORD
);
}
}
src/main/java/com/devteria/identityservice/service/EmailService.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
service
;
import
com.devteria.identityservice.enums.OtpType
;
import
jakarta.mail.MessagingException
;
import
jakarta.mail.internet.MimeMessage
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.mail.javamail.JavaMailSender
;
import
org.springframework.mail.javamail.MimeMessageHelper
;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Service
;
@Service
@Slf4j
@RequiredArgsConstructor
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
EmailService
{
// @Lazy
// final EmailService asyncEmailService;
final
JavaMailSender
mailSender
;
@Async
public
void
sendEmail
(
String
to
,
String
subject
,
String
text
,
boolean
isHtml
)
throws
MessagingException
{
try
{
MimeMessage
message
=
mailSender
.
createMimeMessage
();
MimeMessageHelper
helper
=
new
MimeMessageHelper
(
message
,
true
);
String
fromEmail
=
"hello@demomailtrap.co"
;
helper
.
setFrom
(
fromEmail
);
helper
.
setTo
(
to
);
helper
.
setSubject
(
subject
);
helper
.
setText
(
text
,
isHtml
);
log
.
info
(
"Sending email to {} from {}"
,
to
,
fromEmail
);
mailSender
.
send
(
message
);
log
.
info
(
"Email sent successfully to: {}"
,
to
);
}
catch
(
MessagingException
e
)
{
log
.
error
(
"Failed to send email to {}: {}"
,
to
,
e
.
getMessage
());
throw
e
;
}
}
public
void
sendOtpEmail
(
String
to
,
String
otp
,
OtpType
otpType
)
throws
MessagingException
{
switch
(
otpType
)
{
case
RESET_PASSWORD:
sendPasswordResetOtp
(
to
,
otp
);
break
;
case
REGISTRATION:
sendRegistrationOtp
(
to
,
otp
);
break
;
default
:
log
.
error
(
"Unknown OTP type: {}"
,
otpType
);
throw
new
IllegalArgumentException
(
"Không hỗ trợ loại OTP này"
);
}
}
private
void
sendRegistrationOtp
(
String
to
,
String
otp
)
throws
MessagingException
{
String
subject
=
"Mã OTP xác thực tài khoản của bạn"
;
String
htmlContent
=
String
.
format
(
"<div style='font-family: Arial, sans-serif;'>"
+
"<h2>Xác thực tài khoản</h2>"
+
"<p>Mã OTP của bạn để xác thực tài khoản là:</p>"
+
"<h1 style='color: #4285f4; font-size: 32px; letter-spacing: 2px;'>%s</h1>"
+
"<p>Mã này có hiệu lực trong 5 phút.</p>"
+
"<p>Nếu bạn không yêu cầu xác thực tài khoản, vui lòng bỏ qua email này.</p>"
+
"</div>"
,
otp
);
sendEmail
(
to
,
subject
,
htmlContent
,
true
);
}
private
void
sendPasswordResetOtp
(
String
to
,
String
otp
)
throws
MessagingException
{
String
subject
=
"Mã OTP đổi mật khẩu của bạn"
;
String
htmlContent
=
String
.
format
(
"<div style='font-family: Arial, sans-serif;'>"
+
"<h2>Yêu cầu đổi mật khẩu</h2>"
+
"<p>Mã OTP của bạn để đổi mật khẩu là:</p>"
+
"<h1 style='color: #4285f4; font-size: 32px; letter-spacing: 2px;'>%s</h1>"
+
"<p>Mã này có hiệu lực trong 5 phút.</p>"
+
"<p>Nếu bạn không yêu cầu đổi mật khẩu, vui lòng bỏ qua email này.</p>"
+
"</div>"
,
otp
);
sendEmail
(
to
,
subject
,
htmlContent
,
true
);
}
}
src/main/java/com/devteria/identityservice/service/OtpService.java
0 → 100644
View file @
58a1b9d6
package
com
.
devteria
.
identityservice
.
service
;
import
com.devteria.identityservice.enums.OtpType
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.data.redis.core.RedisTemplate
;
import
org.springframework.stereotype.Service
;
import
java.security.SecureRandom
;
import
java.util.concurrent.TimeUnit
;
@Service
@RequiredArgsConstructor
@Slf4j
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
public
class
OtpService
{
static
final
String
OTP_PREFIX
=
"otp:"
;
static
final
int
OTP_LENGTH
=
6
;
RedisTemplate
<
String
,
Object
>
redisTemplate
;
public
String
generateAndSaveOtp
(
String
identifier
,
OtpType
otpType
)
{
String
otp
=
generateOtp
(
OTP_LENGTH
);
String
key
=
OTP_PREFIX
+
otpType
.
getName
()
+
identifier
;
redisTemplate
.
opsForValue
().
set
(
key
,
otp
,
otpType
.
getExpireTimeInSeconds
(),
TimeUnit
.
SECONDS
);
log
.
info
(
"Generated OTP for {}: {} with type {} (expires in {} seconds)"
,
identifier
,
key
,
otpType
,
otpType
.
getExpireTimeInSeconds
());
return
otp
;
}
public
boolean
validateOtp
(
String
otp
,
String
identifier
,
OtpType
otpType
)
{
String
key
=
OTP_PREFIX
+
otpType
.
getName
()
+
identifier
;
String
storedOtp
=
(
String
)
redisTemplate
.
opsForValue
().
get
(
key
);
if
(
storedOtp
!=
null
&&
storedOtp
.
equals
(
otp
))
{
// Delete OTP after successful validation
redisTemplate
.
delete
(
key
);
return
true
;
}
return
false
;
}
private
String
generateOtp
(
int
length
)
{
SecureRandom
random
=
new
SecureRandom
();
StringBuilder
otp
=
new
StringBuilder
();
for
(
int
i
=
0
;
i
<
length
;
i
++)
{
otp
.
append
(
random
.
nextInt
(
10
));
}
return
otp
.
toString
();
}
}
src/main/java/com/devteria/identityservice/service/UserService.java
View file @
58a1b9d6
...
...
@@ -30,6 +30,7 @@ import java.util.List;
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
@Slf4j
public
class
UserService
{
OtpService
otpService
;
UserRepository
userRepository
;
UserMapper
userMapper
;
PasswordEncoder
passwordEncoder
;
...
...
@@ -82,7 +83,7 @@ public class UserService {
public
UserResponse
getMyInfo
()
{
String
name
=
SecurityContextHolder
.
getContext
().
getAuthentication
().
getName
();
User
user
=
userRepository
.
findBy
Username
(
name
).
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
));
User
user
=
userRepository
.
findBy
Id
(
name
).
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
));
return
userMapper
.
toUserResponse
(
user
);
}
...
...
src/main/resources/application.yaml
View file @
58a1b9d6
...
...
@@ -12,6 +12,22 @@ spring:
hibernate
:
ddl-auto
:
update
show-sql
:
true
properties
:
hibernate
:
format_sql
:
true
mail
:
username
:
api
password
:
476d44bdd59d2aa8c8bf66934f8869fd
host
:
live.smtp.mailtrap.io
port
:
587
properties
:
mail
:
smtp
:
auth
:
true
starttls
:
enable
:
true
ssl
:
enable
:
false
jwt
:
signer-key
:
"
1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuG5SIf/1y/LgJxHnMqaF6A/ij"
...
...
@@ -24,3 +40,4 @@ redis:
password
:
123456a@
time-to-live
:
60
db
:
15
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment