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
d53a2ff2
Commit
d53a2ff2
authored
Apr 23, 2025
by
Phùng Quốc Toàn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement JWT refresh and logout functionality, add Redis caching support
parent
1d64944a
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
638 additions
and
53 deletions
+638
-53
pom.xml
pom.xml
+8
-0
IdentityServiceApplication.java
.../devteria/identityservice/IdentityServiceApplication.java
+3
-3
CustomJwtDecoder.java
...teria/identityservice/configuration/CustomJwtDecoder.java
+54
-0
JwtAuthenticationEntryPoint.java
...ityservice/configuration/JwtAuthenticationEntryPoint.java
+2
-0
RedisConfig.java
...m/devteria/identityservice/configuration/RedisConfig.java
+91
-0
SecurityConfig.java
...evteria/identityservice/configuration/SecurityConfig.java
+6
-17
AuthenticationController.java
.../identityservice/controller/AuthenticationController.java
+18
-4
PermissionController.java
...eria/identityservice/controller/PermissionController.java
+2
-2
RoleController.java
...m/devteria/identityservice/controller/RoleController.java
+44
-0
LogoutRequest.java
...m/devteria/identityservice/dto/request/LogoutRequest.java
+14
-0
RefreshTokenRequest.java
...eria/identityservice/dto/request/RefreshTokenRequest.java
+14
-0
RoleRequest.java
...com/devteria/identityservice/dto/request/RoleRequest.java
+18
-0
UserCreationRequest.java
...eria/identityservice/dto/request/UserCreationRequest.java
+3
-1
UserUpdateRequest.java
...vteria/identityservice/dto/request/UserUpdateRequest.java
+7
-0
PermissionResponse.java
...eria/identityservice/dto/response/PermissionResponse.java
+12
-1
RoleResponse.java
...m/devteria/identityservice/dto/response/RoleResponse.java
+20
-0
InvalidatedToken.java
...com/devteria/identityservice/entity/InvalidatedToken.java
+21
-0
ErrorCode.java
...ava/com/devteria/identityservice/exception/ErrorCode.java
+3
-2
GlobalExceptionHandler.java
...ria/identityservice/exception/GlobalExceptionHandler.java
+17
-1
RoleMapper.java
.../java/com/devteria/identityservice/mapper/RoleMapper.java
+17
-0
UserMapper.java
.../java/com/devteria/identityservice/mapper/UserMapper.java
+2
-0
InvalidatedTokenRepository.java
...dentityservice/repository/InvalidatedTokenRepository.java
+9
-0
RoleRepository.java
...m/devteria/identityservice/repository/RoleRepository.java
+9
-0
AuthenticationService.java
...vteria/identityservice/service/AuthenticationService.java
+123
-16
RoleService.java
...ava/com/devteria/identityservice/service/RoleService.java
+48
-0
UserService.java
...ava/com/devteria/identityservice/service/UserService.java
+14
-4
DobConstraint.java
...com/devteria/identityservice/validator/DobConstraint.java
+23
-0
DobValidator.java
.../com/devteria/identityservice/validator/DobValidator.java
+26
-0
application.yaml
src/main/resources/application.yaml
+10
-2
No files found.
pom.xml
View file @
d53a2ff2
...
...
@@ -66,6 +66,14 @@
<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>
<build>
...
...
src/main/java/com/devteria/identityservice/IdentityServiceApplication.java
View file @
d53a2ff2
...
...
@@ -5,8 +5,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public
class
IdentityServiceApplication
{
public
static
void
main
(
String
[]
args
)
{
SpringApplication
.
run
(
IdentityServiceApplication
.
class
,
args
);
}
public
static
void
main
(
String
[]
args
)
{
SpringApplication
.
run
(
IdentityServiceApplication
.
class
,
args
);
}
}
src/main/java/com/devteria/identityservice/configuration/CustomJwtDecoder.java
0 → 100644
View file @
d53a2ff2
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
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.security.oauth2.jose.jws.MacAlgorithm
;
import
org.springframework.security.oauth2.jwt.Jwt
;
import
org.springframework.security.oauth2.jwt.JwtDecoder
;
import
org.springframework.security.oauth2.jwt.JwtException
;
import
org.springframework.security.oauth2.jwt.NimbusJwtDecoder
;
import
org.springframework.stereotype.Component
;
import
javax.crypto.spec.SecretKeySpec
;
import
java.text.ParseException
;
import
java.util.Objects
;
@Component
public
class
CustomJwtDecoder
implements
JwtDecoder
{
@Value
(
"${jwt.signer-key}"
)
private
String
signerKey
;
@Autowired
private
AuthenticationService
authenticationService
;
private
NimbusJwtDecoder
nimbusJwtDecoder
=
null
;
@Override
public
Jwt
decode
(
String
token
)
throws
JwtException
{
try
{
var
response
=
authenticationService
.
introspect
(
IntrospectRequest
.
builder
()
.
token
(
token
)
.
build
());
if
(!
response
.
isValid
())
throw
new
JwtException
(
"Token invalid"
);
}
catch
(
JOSEException
|
ParseException
e
)
{
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/JwtAuthenticationEntryPoint.java
View file @
d53a2ff2
...
...
@@ -8,9 +8,11 @@ import jakarta.servlet.http.HttpServletRequest;
import
jakarta.servlet.http.HttpServletResponse
;
import
org.springframework.security.core.AuthenticationException
;
import
org.springframework.security.web.AuthenticationEntryPoint
;
import
org.springframework.stereotype.Component
;
import
java.io.IOException
;
@Component
public
class
JwtAuthenticationEntryPoint
implements
AuthenticationEntryPoint
{
@Override
public
void
commence
(
HttpServletRequest
request
,
HttpServletResponse
response
,
AuthenticationException
authException
)
throws
IOException
,
ServletException
{
...
...
src/main/java/com/devteria/identityservice/configuration/RedisConfig.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
configuration
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.cache.annotation.EnableCaching
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.context.annotation.Primary
;
import
org.springframework.data.redis.cache.RedisCacheConfiguration
;
import
org.springframework.data.redis.cache.RedisCacheManager
;
import
org.springframework.data.redis.connection.RedisConnectionFactory
;
import
org.springframework.data.redis.connection.RedisStandaloneConfiguration
;
import
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
;
import
org.springframework.data.redis.core.RedisTemplate
;
import
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
;
import
org.springframework.data.redis.serializer.RedisSerializationContext
;
import
org.springframework.data.redis.serializer.StringRedisSerializer
;
import
java.time.Duration
;
@Configuration
@EnableCaching
public
class
RedisConfig
{
@Value
(
"${redis.host}"
)
private
String
redisHost
;
@Value
(
"${redis.port}"
)
private
int
redisPort
;
@Value
(
"${redis.password:}"
)
private
String
redisPassword
;
@Value
(
"${redis.time-to-live}"
)
private
long
timeToLive
;
@Bean
public
LettuceConnectionFactory
redisConnectionFactory
()
{
RedisStandaloneConfiguration
redisConfig
=
new
RedisStandaloneConfiguration
();
redisConfig
.
setHostName
(
redisHost
);
redisConfig
.
setPort
(
redisPort
);
if
(!
redisPassword
.
isEmpty
())
{
redisConfig
.
setPassword
(
redisPassword
);
}
return
new
LettuceConnectionFactory
(
redisConfig
);
}
@Bean
@Primary
public
RedisTemplate
<
String
,
Object
>
redisTemplate
(
RedisConnectionFactory
connectionFactory
)
{
RedisTemplate
<
String
,
Object
>
template
=
new
RedisTemplate
<>();
template
.
setConnectionFactory
(
connectionFactory
);
template
.
setKeySerializer
(
stringRedisSerializer
());
template
.
setValueSerializer
(
genericJackson2JsonRedisSerializer
());
template
.
setHashKeySerializer
(
stringRedisSerializer
());
template
.
setHashValueSerializer
(
genericJackson2JsonRedisSerializer
());
template
.
afterPropertiesSet
();
return
template
;
}
@Bean
public
StringRedisSerializer
stringRedisSerializer
()
{
return
new
StringRedisSerializer
();
}
@Bean
public
ObjectMapper
objectMapper
()
{
ObjectMapper
objectMapper
=
new
ObjectMapper
();
objectMapper
.
registerModule
(
new
JavaTimeModule
());
return
objectMapper
;
}
@Bean
public
GenericJackson2JsonRedisSerializer
genericJackson2JsonRedisSerializer
()
{
return
new
GenericJackson2JsonRedisSerializer
(
objectMapper
());
}
@Bean
public
RedisCacheManager
cacheManager
(
RedisConnectionFactory
connectionFactory
)
{
RedisCacheConfiguration
cacheConfig
=
RedisCacheConfiguration
.
defaultCacheConfig
()
.
entryTtl
(
Duration
.
ofSeconds
(
timeToLive
))
.
serializeKeysWith
(
RedisSerializationContext
.
SerializationPair
.
fromSerializer
(
stringRedisSerializer
()))
.
serializeValuesWith
(
RedisSerializationContext
.
SerializationPair
.
fromSerializer
(
genericJackson2JsonRedisSerializer
()))
.
disableCachingNullValues
();
return
RedisCacheManager
.
builder
(
connectionFactory
)
.
cacheDefaults
(
cacheConfig
)
.
build
();
}
}
src/main/java/com/devteria/identityservice/configuration/SecurityConfig.java
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
configuration
;
import
org.springframework.beans.factory.annotation.Value
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.http.HttpMethod
;
...
...
@@ -10,24 +10,19 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.security.oauth2.jose.jws.MacAlgorithm
;
import
org.springframework.security.oauth2.jwt.JwtDecoder
;
import
org.springframework.security.oauth2.jwt.NimbusJwtDecoder
;
import
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
;
import
org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
;
import
org.springframework.security.web.SecurityFilterChain
;
import
javax.crypto.spec.SecretKeySpec
;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public
class
SecurityConfig
{
private
final
String
[]
PUBLIC_ENDPOINTS
=
new
String
[]{
"/auth/token"
,
"/auth/introspect"
,
"/users"
};
private
final
String
[]
PUBLIC_ENDPOINTS
=
new
String
[]{
"/auth/token"
,
"/auth/introspect"
,
"/users"
,
"/auth/logout"
,
"/auth/refresh-token"
};
@Value
(
"${jwt.signerKey}"
)
private
String
signerKey
;
CustomJwtDecoder
customJwtDecoder
;
@Bean
public
SecurityFilterChain
filterChain
(
HttpSecurity
httpSecurity
)
throws
Exception
{
...
...
@@ -38,7 +33,7 @@ public class SecurityConfig {
httpSecurity
.
oauth2ResourceServer
(
oauth2
->
oauth2
.
jwt
(
jwtConfigurer
->
jwtConfigurer
.
decoder
(
jwtDecoder
()
)
jwtConfigurer
.
decoder
(
customJwtDecoder
)
.
jwtAuthenticationConverter
(
jwtConverter
()))
.
authenticationEntryPoint
(
new
JwtAuthenticationEntryPoint
()));
...
...
@@ -50,7 +45,7 @@ public class SecurityConfig {
@Bean
JwtAuthenticationConverter
jwtConverter
()
{
JwtGrantedAuthoritiesConverter
jwtGrantedAuthoritiesConverter
=
new
JwtGrantedAuthoritiesConverter
();
jwtGrantedAuthoritiesConverter
.
setAuthorityPrefix
(
"
ROLE_
"
);
jwtGrantedAuthoritiesConverter
.
setAuthorityPrefix
(
""
);
JwtAuthenticationConverter
jwtAuthenticationConverter
=
new
JwtAuthenticationConverter
();
jwtAuthenticationConverter
.
setJwtGrantedAuthoritiesConverter
(
jwtGrantedAuthoritiesConverter
);
...
...
@@ -58,12 +53,6 @@ public class SecurityConfig {
return
jwtAuthenticationConverter
;
}
@Bean
JwtDecoder
jwtDecoder
()
{
SecretKeySpec
secretKeySpec
=
new
SecretKeySpec
(
signerKey
.
getBytes
(),
"HS512"
);
return
NimbusJwtDecoder
.
withSecretKey
(
secretKeySpec
).
macAlgorithm
(
MacAlgorithm
.
HS512
).
build
();
}
@Bean
public
PasswordEncoder
passwordEncoder
()
{
return
new
BCryptPasswordEncoder
(
10
);
...
...
src/main/java/com/devteria/identityservice/controller/AuthenticationController.java
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
controller
;
import
com.devteria.identityservice.dto.request.ApiResponse
;
import
com.devteria.identityservice.dto.request.AuthenticationRequest
;
import
com.devteria.identityservice.dto.request.IntrospectRequest
;
import
com.devteria.identityservice.dto.request.*
;
import
com.devteria.identityservice.dto.response.AuthenticationResponse
;
import
com.devteria.identityservice.dto.response.IntrospectResponse
;
import
com.devteria.identityservice.service.AuthenticationService
;
...
...
@@ -25,13 +23,21 @@ public class AuthenticationController {
AuthenticationService
authenticationService
;
@PostMapping
(
"/token"
)
ApiResponse
<
AuthenticationResponse
>
authenticate
(
@RequestBody
AuthenticationRequest
request
){
ApiResponse
<
AuthenticationResponse
>
authenticate
(
@RequestBody
AuthenticationRequest
request
)
{
var
result
=
authenticationService
.
authenticate
(
request
);
return
ApiResponse
.<
AuthenticationResponse
>
builder
()
.
result
(
result
)
.
build
();
}
@PostMapping
(
"/refresh-token"
)
ApiResponse
<
AuthenticationResponse
>
refreshToken
(
@RequestBody
RefreshTokenRequest
request
)
throws
ParseException
,
JOSEException
{
var
result
=
authenticationService
.
refreshToken
(
request
);
return
ApiResponse
.<
AuthenticationResponse
>
builder
()
.
result
(
result
)
.
build
();
}
@PostMapping
(
"/introspect"
)
ApiResponse
<
IntrospectResponse
>
authenticate
(
@RequestBody
IntrospectRequest
request
)
throws
ParseException
,
JOSEException
{
...
...
@@ -40,4 +46,12 @@ public class AuthenticationController {
.
result
(
result
)
.
build
();
}
@PostMapping
(
"/logout"
)
ApiResponse
<
String
>
logout
(
@RequestBody
LogoutRequest
request
)
throws
ParseException
,
JOSEException
{
authenticationService
.
logout
(
request
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Logout successful"
)
.
build
();
}
}
src/main/java/com/devteria/identityservice/controller/PermissionController.java
View file @
d53a2ff2
...
...
@@ -34,8 +34,8 @@ public class PermissionController {
.
build
();
}
@DeleteMapping
ApiResponse
<
String
>
delete
(
@
RequestParam
String
permission
)
{
@DeleteMapping
(
"/{permission}"
)
ApiResponse
<
String
>
delete
(
@
PathVariable
String
permission
)
{
permissionService
.
delete
(
permission
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Permission deleted successfully"
)
...
...
src/main/java/com/devteria/identityservice/controller/RoleController.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
controller
;
import
com.devteria.identityservice.dto.request.ApiResponse
;
import
com.devteria.identityservice.dto.request.RoleRequest
;
import
com.devteria.identityservice.dto.response.RoleResponse
;
import
com.devteria.identityservice.service.RoleService
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.web.bind.annotation.*
;
import
java.util.List
;
@RestController
@RequestMapping
(
"/roles"
)
@RequiredArgsConstructor
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
@Slf4j
public
class
RoleController
{
RoleService
roleService
;
@PostMapping
ApiResponse
<
RoleResponse
>
create
(
@RequestBody
RoleRequest
request
)
{
return
ApiResponse
.<
RoleResponse
>
builder
()
.
result
(
roleService
.
create
(
request
))
.
build
();
}
@GetMapping
ApiResponse
<
List
<
RoleResponse
>>
getAll
()
{
return
ApiResponse
.<
List
<
RoleResponse
>>
builder
()
.
result
(
roleService
.
getAll
())
.
build
();
}
@DeleteMapping
(
"/{role}"
)
ApiResponse
<
String
>
delete
(
@PathVariable
String
role
)
{
roleService
.
delete
(
role
);
return
ApiResponse
.<
String
>
builder
()
.
result
(
"Role deleted successfully"
)
.
build
();
}
}
src/main/java/com/devteria/identityservice/dto/request/LogoutRequest.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
LogoutRequest
{
String
token
;
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/dto/request/RefreshTokenRequest.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
RefreshTokenRequest
{
String
token
;
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/dto/request/RoleRequest.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
import
java.util.Set
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
RoleRequest
{
String
name
;
String
description
;
Set
<
String
>
permissions
;
}
src/main/java/com/devteria/identityservice/dto/request/UserCreationRequest.java
View file @
d53a2ff2
...
...
@@ -12,12 +12,14 @@ import java.time.LocalDate;
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
UserCreationRequest
{
@Size
(
min
=
3
,
message
=
"USERNAME_INVALID"
)
@Size
(
min
=
3
,
message
=
"USERNAME_INVALID"
)
String
username
;
@Size
(
min
=
8
,
message
=
"INVALID_PASSWORD"
)
String
password
;
String
firstName
;
String
lastName
;
// @DobConstraint(min = 16, message = "INVALID_DOB")
LocalDate
dob
;
}
src/main/java/com/devteria/identityservice/dto/request/UserUpdateRequest.java
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
request
;
import
com.devteria.identityservice.validator.DobConstraint
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
import
java.time.LocalDate
;
import
java.util.List
;
@Data
@Builder
...
...
@@ -14,5 +16,10 @@ public class UserUpdateRequest {
String
password
;
String
firstName
;
String
lastName
;
@DobConstraint
(
min
=
18
,
message
=
"INVALID_DOB"
)
LocalDate
dob
;
List
<
String
>
roles
;
}
src/main/java/com/devteria/identityservice/dto/response/PermissionResponse.java
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
response
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
PermissionResponse
{
}
String
name
;
String
description
;
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/dto/response/RoleResponse.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
dto
.
response
;
import
com.devteria.identityservice.entity.Permission
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
import
java.util.Set
;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
public
class
RoleResponse
{
String
name
;
String
description
;
Set
<
Permission
>
permissions
;
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/entity/InvalidatedToken.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
entity
;
import
jakarta.persistence.Entity
;
import
jakarta.persistence.Id
;
import
lombok.*
;
import
lombok.experimental.FieldDefaults
;
import
java.util.Date
;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
)
@Entity
public
class
InvalidatedToken
{
@Id
String
id
;
Date
expiryTime
;
}
src/main/java/com/devteria/identityservice/exception/ErrorCode.java
View file @
d53a2ff2
...
...
@@ -9,11 +9,12 @@ public enum ErrorCode {
UNCATEGORIZED_EXCEPTION
(
9999
,
"Uncategorized error"
,
HttpStatus
.
INTERNAL_SERVER_ERROR
),
INVALID_KEY
(
1001
,
"Uncategorized error"
,
HttpStatus
.
BAD_REQUEST
),
USER_EXISTED
(
1002
,
"User existed"
,
HttpStatus
.
BAD_REQUEST
),
USERNAME_INVALID
(
1003
,
"Username must be at least
3
characters"
,
HttpStatus
.
BAD_REQUEST
),
INVALID_PASSWORD
(
1004
,
"Password must be at least
8
characters"
,
HttpStatus
.
BAD_REQUEST
),
USERNAME_INVALID
(
1003
,
"Username must be at least
{min}
characters"
,
HttpStatus
.
BAD_REQUEST
),
INVALID_PASSWORD
(
1004
,
"Password must be at least
{min}
characters"
,
HttpStatus
.
BAD_REQUEST
),
USER_NOT_EXISTED
(
1005
,
"User not existed"
,
HttpStatus
.
NOT_FOUND
),
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
),
;
...
...
src/main/java/com/devteria/identityservice/exception/GlobalExceptionHandler.java
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
exception
;
import
com.devteria.identityservice.dto.request.ApiResponse
;
import
jakarta.validation.ConstraintViolation
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.security.access.AccessDeniedException
;
...
...
@@ -8,9 +9,13 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import
org.springframework.web.bind.annotation.ControllerAdvice
;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
java.util.Map
;
import
java.util.Objects
;
@ControllerAdvice
@Slf4j
public
class
GlobalExceptionHandler
{
private
static
final
String
MIN_ATTRIBUTE
=
"min"
;
@ExceptionHandler
(
value
=
Exception
.
class
)
ResponseEntity
<
ApiResponse
>
handlingRuntimeException
(
RuntimeException
exception
)
{
...
...
@@ -40,8 +45,13 @@ public class GlobalExceptionHandler {
ErrorCode
errorCode
=
ErrorCode
.
INVALID_KEY
;
Map
<
String
,
Object
>
attributes
=
null
;
try
{
errorCode
=
ErrorCode
.
valueOf
(
enumKey
);
var
constraintViolation
=
exception
.
getBindingResult
().
getAllErrors
().
getFirst
().
unwrap
(
ConstraintViolation
.
class
);
attributes
=
constraintViolation
.
getConstraintDescriptor
().
getAttributes
();
log
.
info
(
attributes
.
toString
());
}
catch
(
IllegalArgumentException
e
)
{
}
...
...
@@ -49,7 +59,7 @@ public class GlobalExceptionHandler {
ApiResponse
apiResponse
=
new
ApiResponse
();
apiResponse
.
setCode
(
errorCode
.
getCode
());
apiResponse
.
setMessage
(
errorCode
.
getMessage
());
apiResponse
.
setMessage
(
Objects
.
nonNull
(
attributes
)
?
mapAttribute
(
errorCode
.
getMessage
(),
attributes
)
:
errorCode
.
getMessage
());
return
ResponseEntity
.
badRequest
().
body
(
apiResponse
);
}
...
...
@@ -60,4 +70,10 @@ public class GlobalExceptionHandler {
return
ResponseEntity
.
status
(
errorCode
.
getStatusCode
()).
body
(
ApiResponse
.
builder
().
code
(
errorCode
.
getCode
()).
message
(
errorCode
.
getMessage
()).
build
());
}
private
String
mapAttribute
(
String
message
,
Map
<
String
,
Object
>
attributes
)
{
String
minValue
=
attributes
.
get
(
MIN_ATTRIBUTE
).
toString
();
return
message
.
replace
(
"{"
+
MIN_ATTRIBUTE
+
"}"
,
minValue
);
}
}
src/main/java/com/devteria/identityservice/mapper/RoleMapper.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
mapper
;
import
com.devteria.identityservice.dto.request.RoleRequest
;
import
com.devteria.identityservice.dto.response.RoleResponse
;
import
com.devteria.identityservice.entity.Role
;
import
org.mapstruct.Mapper
;
import
org.mapstruct.Mapping
;
@Mapper
(
componentModel
=
"spring"
)
public
interface
RoleMapper
{
@Mapping
(
target
=
"permissions"
,
ignore
=
true
)
Role
toRole
(
RoleRequest
request
);
RoleResponse
toRoleResponse
(
Role
role
);
}
\ No newline at end of file
src/main/java/com/devteria/identityservice/mapper/UserMapper.java
View file @
d53a2ff2
...
...
@@ -5,6 +5,7 @@ import com.devteria.identityservice.dto.request.UserUpdateRequest;
import
com.devteria.identityservice.dto.response.UserResponse
;
import
com.devteria.identityservice.entity.User
;
import
org.mapstruct.Mapper
;
import
org.mapstruct.Mapping
;
import
org.mapstruct.MappingTarget
;
@Mapper
(
componentModel
=
"spring"
)
...
...
@@ -13,5 +14,6 @@ public interface UserMapper {
UserResponse
toUserResponse
(
User
user
);
@Mapping
(
target
=
"roles"
,
ignore
=
true
)
void
updateUser
(
@MappingTarget
User
user
,
UserUpdateRequest
request
);
}
src/main/java/com/devteria/identityservice/repository/InvalidatedTokenRepository.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
repository
;
import
com.devteria.identityservice.entity.InvalidatedToken
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.stereotype.Repository
;
@Repository
public
interface
InvalidatedTokenRepository
extends
JpaRepository
<
InvalidatedToken
,
String
>
{
}
src/main/java/com/devteria/identityservice/repository/RoleRepository.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
repository
;
import
com.devteria.identityservice.entity.Role
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.stereotype.Repository
;
@Repository
public
interface
RoleRepository
extends
JpaRepository
<
Role
,
String
>
{
}
src/main/java/com/devteria/identityservice/service/AuthenticationService.java
View file @
d53a2ff2
...
...
@@ -2,8 +2,11 @@ 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.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.exception.AppException
;
import
com.devteria.identityservice.exception.ErrorCode
;
...
...
@@ -19,42 +22,55 @@ import lombok.experimental.FieldDefaults;
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.crypto.bcrypt.BCryptPasswordEncoder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.CollectionUtils
;
import
java.text.ParseException
;
import
java.time.Instant
;
import
java.time.temporal.ChronoUnit
;
import
java.util.Date
;
import
java.util.StringJoiner
;
import
java.util.UUID
;
import
java.util.concurrent.TimeUnit
;
@Service
@RequiredArgsConstructor
@Slf4j
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
public
class
AuthenticationService
{
private
static
final
String
TOKEN_KEY_PREFIX
=
"invalid_token:"
;
RedisTemplate
<
String
,
Object
>
redisTemplate
;
UserRepository
userRepository
;
@NonFinal
@Value
(
"${jwt.signer
K
ey}"
)
@Value
(
"${jwt.signer
-k
ey}"
)
protected
String
SIGNER_KEY
;
@NonFinal
@Value
(
"${jwt.expiration-time}"
)
protected
Long
EXPIRATION_TIME
;
@NonFinal
@Value
(
"${jwt.refresh-expiration-time}"
)
protected
Long
REFRESH_EXPIRATION_TIME
;
public
IntrospectResponse
introspect
(
IntrospectRequest
request
)
throws
JOSEException
,
ParseException
{
var
token
=
request
.
getToken
();
JWSVerifier
verifier
=
new
MACVerifier
(
SIGNER_KEY
.
getBytes
());
SignedJWT
signedJWT
=
SignedJWT
.
parse
(
token
);
Date
expiryTime
=
signedJWT
.
getJWTClaimsSet
().
getExpirationTime
();
var
verified
=
signedJWT
.
verify
(
verifier
);
boolean
isValid
=
true
;
try
{
verifyToken
(
token
,
false
);
}
catch
(
AppException
a
)
{
isValid
=
false
;
}
return
IntrospectResponse
.
builder
()
.
valid
(
verified
&&
expiryTime
.
after
(
new
Date
())
)
.
valid
(
isValid
)
.
build
();
}
public
AuthenticationResponse
authenticate
(
AuthenticationRequest
request
)
{
...
...
@@ -83,8 +99,9 @@ public class AuthenticationService {
.
issuer
(
"demo_jwt"
)
.
issueTime
(
new
Date
())
.
expirationTime
(
new
Date
(
Instant
.
now
().
plus
(
1
,
ChronoUnit
.
HOUR
S
).
toEpochMilli
()
Instant
.
now
().
plus
(
EXPIRATION_TIME
,
ChronoUnit
.
SECOND
S
).
toEpochMilli
()
))
.
jwtID
(
UUID
.
randomUUID
().
toString
())
.
claim
(
"scope"
,
buildScope
(
user
))
.
build
();
...
...
@@ -102,12 +119,102 @@ public class AuthenticationService {
}
private
String
buildScope
(
User
user
)
{
StringJoiner
stringJoiner
=
new
StringJoiner
(
""
);
//
// if (!CollectionUtils.isEmpty(user.getRoles())) {
// user.getRoles().forEach(stringJoiner::add);
// }
StringJoiner
stringJoiner
=
new
StringJoiner
(
" "
);
if
(!
CollectionUtils
.
isEmpty
(
user
.
getRoles
()))
{
user
.
getRoles
().
forEach
(
role
->
{
stringJoiner
.
add
(
"ROLE_"
+
role
.
getName
());
if
(!
CollectionUtils
.
isEmpty
(
role
.
getPermissions
()))
{
role
.
getPermissions
().
forEach
(
permission
->
stringJoiner
.
add
(
permission
.
getName
())
);
}
});
}
return
stringJoiner
.
toString
();
}
public
void
logout
(
LogoutRequest
request
)
throws
ParseException
,
JOSEException
{
SignedJWT
signedJWT
=
null
;
try
{
signedJWT
=
verifyToken
(
request
.
getToken
(),
true
);
}
catch
(
AppException
a
)
{
log
.
info
(
"Token already expired or invalidated"
);
}
String
jit
=
signedJWT
.
getJWTClaimsSet
().
getJWTID
();
Date
expiryTime
=
signedJWT
.
getJWTClaimsSet
().
getExpirationTime
();
InvalidatedToken
invalidatedToken
=
InvalidatedToken
.
builder
()
.
id
(
jit
)
.
expiryTime
(
expiryTime
)
.
build
();
saveInvalidatedToken
(
invalidatedToken
);
}
private
SignedJWT
verifyToken
(
String
token
,
boolean
isRefresh
)
throws
ParseException
,
JOSEException
{
JWSVerifier
verifier
=
new
MACVerifier
(
SIGNER_KEY
.
getBytes
());
SignedJWT
signedJWT
=
SignedJWT
.
parse
(
token
);
Date
expiryTime
=
!
isRefresh
?
signedJWT
.
getJWTClaimsSet
().
getExpirationTime
()
:
new
Date
(
signedJWT
.
getJWTClaimsSet
().
getIssueTime
()
.
toInstant
().
plus
(
REFRESH_EXPIRATION_TIME
,
ChronoUnit
.
SECONDS
).
toEpochMilli
());
var
verified
=
signedJWT
.
verify
(
verifier
);
if
(!(
verified
&&
expiryTime
.
after
(
new
Date
())))
{
throw
new
AppException
(
ErrorCode
.
UNAUTHENTICATED
);
}
if
(
existsInvalidatedToken
(
signedJWT
.
getJWTClaimsSet
().
getJWTID
()))
{
throw
new
AppException
(
ErrorCode
.
UNAUTHENTICATED
);
}
return
signedJWT
;
}
public
AuthenticationResponse
refreshToken
(
RefreshTokenRequest
request
)
throws
ParseException
,
JOSEException
{
var
signedJWT
=
verifyToken
(
request
.
getToken
(),
true
);
String
jit
=
signedJWT
.
getJWTClaimsSet
().
getJWTID
();
Date
expiryTime
=
signedJWT
.
getJWTClaimsSet
().
getExpirationTime
();
InvalidatedToken
invalidatedToken
=
InvalidatedToken
.
builder
()
.
id
(
jit
)
.
expiryTime
(
expiryTime
)
.
build
();
saveInvalidatedToken
(
invalidatedToken
);
String
username
=
signedJWT
.
getJWTClaimsSet
().
getSubject
();
User
user
=
userRepository
.
findByUsername
(
username
)
.
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
UNAUTHENTICATED
));
var
token
=
generateToken
(
user
);
return
AuthenticationResponse
.
builder
()
.
token
(
token
)
.
authenticated
(
true
)
.
build
();
}
private
void
saveInvalidatedToken
(
InvalidatedToken
token
)
{
String
key
=
TOKEN_KEY_PREFIX
+
token
.
getId
();
long
ttlMillis
=
token
.
getExpiryTime
().
getTime
()
-
System
.
currentTimeMillis
();
// Chỉ lưu nếu token chưa hết hạn
if
(
ttlMillis
>
0
)
{
redisTemplate
.
opsForValue
().
set
(
key
,
token
,
ttlMillis
,
TimeUnit
.
MILLISECONDS
);
}
}
private
boolean
existsInvalidatedToken
(
String
tokenId
)
{
return
Boolean
.
TRUE
.
equals
(
redisTemplate
.
hasKey
(
TOKEN_KEY_PREFIX
+
tokenId
));
}
}
src/main/java/com/devteria/identityservice/service/RoleService.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
service
;
import
com.devteria.identityservice.dto.request.RoleRequest
;
import
com.devteria.identityservice.dto.response.RoleResponse
;
import
com.devteria.identityservice.mapper.RoleMapper
;
import
com.devteria.identityservice.repository.PermissionRepository
;
import
com.devteria.identityservice.repository.RoleRepository
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
java.util.HashSet
;
import
java.util.List
;
@Service
@RequiredArgsConstructor
@Slf4j
@FieldDefaults
(
level
=
AccessLevel
.
PRIVATE
,
makeFinal
=
true
)
public
class
RoleService
{
RoleRepository
roleRepository
;
PermissionRepository
permissionRepository
;
RoleMapper
roleMapper
;
public
RoleResponse
create
(
RoleRequest
request
)
{
var
role
=
roleMapper
.
toRole
(
request
);
var
permissions
=
permissionRepository
.
findAllById
(
request
.
getPermissions
());
role
.
setPermissions
(
new
HashSet
<>(
permissions
));
roleRepository
.
save
(
role
);
return
roleMapper
.
toRoleResponse
(
role
);
}
public
List
<
RoleResponse
>
getAll
()
{
var
roles
=
roleRepository
.
findAll
();
return
roles
.
stream
()
.
map
(
roleMapper:
:
toRoleResponse
)
.
toList
();
}
public
void
delete
(
String
id
)
{
roleRepository
.
deleteById
(
id
);
}
}
src/main/java/com/devteria/identityservice/service/UserService.java
View file @
d53a2ff2
...
...
@@ -7,17 +7,22 @@ import com.devteria.identityservice.entity.User;
import
com.devteria.identityservice.exception.AppException
;
import
com.devteria.identityservice.exception.ErrorCode
;
import
com.devteria.identityservice.mapper.UserMapper
;
import
com.devteria.identityservice.repository.RoleRepository
;
import
com.devteria.identityservice.repository.UserRepository
;
import
lombok.AccessLevel
;
import
lombok.RequiredArgsConstructor
;
import
lombok.experimental.FieldDefaults
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.cache.annotation.CacheEvict
;
import
org.springframework.cache.annotation.CachePut
;
import
org.springframework.cache.annotation.Cacheable
;
import
org.springframework.security.access.prepost.PostAuthorize
;
import
org.springframework.security.access.prepost.PreAuthorize
;
import
org.springframework.security.core.context.SecurityContextHolder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.stereotype.Service
;
import
java.util.HashSet
;
import
java.util.List
;
@Service
...
...
@@ -28,6 +33,7 @@ public class UserService {
UserRepository
userRepository
;
UserMapper
userMapper
;
PasswordEncoder
passwordEncoder
;
private
final
RoleRepository
roleRepository
;
public
UserResponse
createUser
(
UserCreationRequest
request
)
{
if
(
userRepository
.
existsByUsername
(
request
.
getUsername
()))
...
...
@@ -36,22 +42,24 @@ public class UserService {
User
user
=
userMapper
.
toUser
(
request
);
user
.
setPassword
(
passwordEncoder
.
encode
(
request
.
getPassword
()));
// HashSet<String> roles = new HashSet<>();
// roles.add(Role.USER.name());
// user.setRoles(roles);
return
userMapper
.
toUserResponse
(
userRepository
.
save
(
user
));
}
@CachePut
(
value
=
"user"
,
key
=
"#userId"
)
public
UserResponse
updateUser
(
String
userId
,
UserUpdateRequest
request
)
{
User
user
=
userRepository
.
findById
(
userId
)
.
orElseThrow
(()
->
new
RuntimeException
(
"User not found"
));
userMapper
.
updateUser
(
user
,
request
);
user
.
setPassword
(
passwordEncoder
.
encode
(
request
.
getPassword
()));
var
roles
=
roleRepository
.
findAllById
(
request
.
getRoles
());
user
.
setRoles
(
new
HashSet
<>(
roles
));
return
userMapper
.
toUserResponse
(
userRepository
.
save
(
user
));
}
@CacheEvict
(
value
=
"user"
,
key
=
"#userId"
)
public
void
deleteUser
(
String
userId
)
{
userRepository
.
deleteById
(
userId
);
}
...
...
@@ -63,6 +71,7 @@ public class UserService {
.
map
(
userMapper:
:
toUserResponse
).
toList
();
}
@Cacheable
(
value
=
"user"
,
key
=
"#id"
)
@PostAuthorize
(
"returnObject.username == authentication.name or hasRole('ADMIN')"
)
public
UserResponse
getUser
(
String
id
)
{
log
.
info
(
"Get user with id: {}"
,
id
);
...
...
@@ -70,6 +79,7 @@ public class UserService {
.
orElseThrow
(()
->
new
RuntimeException
(
"User not found"
)));
}
public
UserResponse
getMyInfo
()
{
String
name
=
SecurityContextHolder
.
getContext
().
getAuthentication
().
getName
();
User
user
=
userRepository
.
findByUsername
(
name
).
orElseThrow
(()
->
new
AppException
(
ErrorCode
.
USER_NOT_EXISTED
));
...
...
src/main/java/com/devteria/identityservice/validator/DobConstraint.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
validator
;
import
jakarta.validation.Constraint
;
import
jakarta.validation.Payload
;
import
java.lang.annotation.Retention
;
import
java.lang.annotation.Target
;
import
static
java
.
lang
.
annotation
.
ElementType
.
FIELD
;
import
static
java
.
lang
.
annotation
.
RetentionPolicy
.
RUNTIME
;
@Target
(
FIELD
)
@Retention
(
RUNTIME
)
@Constraint
(
validatedBy
=
{
DobValidator
.
class
})
public
@interface
DobConstraint
{
String
message
()
default
"Invalid date of birth"
;
int
min
();
Class
<?>[]
groups
()
default
{};
Class
<?
extends
Payload
>[]
payload
()
default
{};
}
src/main/java/com/devteria/identityservice/validator/DobValidator.java
0 → 100644
View file @
d53a2ff2
package
com
.
devteria
.
identityservice
.
validator
;
import
jakarta.validation.ConstraintValidator
;
import
jakarta.validation.ConstraintValidatorContext
;
import
java.time.LocalDate
;
import
java.time.temporal.ChronoUnit
;
public
class
DobValidator
implements
ConstraintValidator
<
DobConstraint
,
LocalDate
>
{
private
int
min
;
@Override
public
void
initialize
(
DobConstraint
constraintAnnotation
)
{
ConstraintValidator
.
super
.
initialize
(
constraintAnnotation
);
min
=
constraintAnnotation
.
min
();
}
@Override
public
boolean
isValid
(
LocalDate
localDate
,
ConstraintValidatorContext
constraintValidatorContext
)
{
if
(
localDate
==
null
)
return
true
;
long
years
=
ChronoUnit
.
YEARS
.
between
(
localDate
,
LocalDate
.
now
());
return
years
>=
min
;
}
}
src/main/resources/application.yaml
View file @
d53a2ff2
...
...
@@ -14,4 +14,13 @@ spring:
show-sql
:
true
jwt
:
signerKey
:
"
1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuG5SIf/1y/LgJxHnMqaF6A/ij"
\ No newline at end of file
signer-key
:
"
1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuG5SIf/1y/LgJxHnMqaF6A/ij"
expiration-time
:
3600
refresh-expiration-time
:
86400
redis
:
host
:
10.3.3.115
port
:
6379
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