Commit 09bbd610 authored by Phùng Quốc Toàn's avatar Phùng Quốc Toàn

Merge branch 'course/issue-and-verify-jwt-token' into 'master'

feat: implement JWT refresh and logout functionality, add Redis caching support

See merge request !2
parents 1d64944a 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>
......
......@@ -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);
}
}
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
......@@ -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 {
......
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();
}
}
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);
......
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();
}
}
......@@ -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")
......
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();
}
}
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
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
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;
}
......@@ -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;
}
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;
}
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
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
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;
}
......@@ -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),
;
......
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);
}
}
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
......@@ -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);
}
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> {
}
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> {
}
......@@ -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.signerKey}")
@Value("${jwt.signer-key}")
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.HOURS).toEpochMilli()
Instant.now().plus(EXPIRATION_TIME, ChronoUnit.SECONDS).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));
}
}
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);
}
}
......@@ -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));
......
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 {};
}
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;
}
}
......@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment