From ff454996b0c29f49d8c8f4b776849a7912e881db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sat, 20 Jul 2024 23:43:16 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat(User):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=97=90=20=EA=B6=8C=ED=95=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stempo/api/domain/domain/model/Role.java | 15 ++++++++++++++ .../stempo/api/domain/domain/model/User.java | 20 +++++++++++++++++++ .../domain/persistence/entity/UserEntity.java | 6 ++++++ 3 files changed, 41 insertions(+) create mode 100644 src/main/java/com/stempo/api/domain/domain/model/Role.java create mode 100644 src/main/java/com/stempo/api/domain/domain/model/User.java diff --git a/src/main/java/com/stempo/api/domain/domain/model/Role.java b/src/main/java/com/stempo/api/domain/domain/model/Role.java new file mode 100644 index 0000000..a907be0 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/model/Role.java @@ -0,0 +1,15 @@ +package com.stempo.api.domain.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Role { + + USER("ROLE_USER", "Normal User"), + ADMIN("ROLE_ADMIN", "Administrator"); + + private final String key; + private final String description; +} diff --git a/src/main/java/com/stempo/api/domain/domain/model/User.java b/src/main/java/com/stempo/api/domain/domain/model/User.java new file mode 100644 index 0000000..a6ccdd7 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/model/User.java @@ -0,0 +1,20 @@ +package com.stempo.api.domain.domain.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class User { + + private String id; + private String password; + private Role role; +} diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java index ce3e79b..8a2b387 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java @@ -1,7 +1,10 @@ package com.stempo.api.domain.persistence.entity; +import com.stempo.api.domain.domain.model.Role; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; @@ -25,4 +28,7 @@ public class UserEntity extends BaseEntity { @Column(nullable = false) private String password; + + @Enumerated(EnumType.STRING) + private Role role; } From a5ec46dffd607ebfefa7208c076ed96bd9afedcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sat, 20 Jul 2024 23:43:53 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat(User):=20API=20Docs=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/global/config/OpenApiConfig.java | 49 +++++++++++++++++++ .../api/global/config/SecurityConfig.java | 27 ++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/main/java/com/stempo/api/global/config/OpenApiConfig.java create mode 100644 src/main/java/com/stempo/api/global/config/SecurityConfig.java diff --git a/src/main/java/com/stempo/api/global/config/OpenApiConfig.java b/src/main/java/com/stempo/api/global/config/OpenApiConfig.java new file mode 100644 index 0000000..f20f1e8 --- /dev/null +++ b/src/main/java/com/stempo/api/global/config/OpenApiConfig.java @@ -0,0 +1,49 @@ +package com.stempo.api.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI(@Value("${springdoc.version}") String appVersion) { + Info info = new Info().title("Stempo").version(appVersion) + .description("Stempo API Document") + .termsOfService("http://swagger.io/terms/") + .contact(new Contact().name("한관희").url("https://github.com/limehee").email("noop103@naver.com")) + .license(new License().name("Stempo License Version 1.0").url("https://github.com/KKKK-Stempo")); + + final String securitySchemeName = "bearerAuth"; + Server server = new Server().url("/"); + + return new OpenAPI() + .servers(List.of(server)) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components( + new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + ) + ) + .info(info); + } +} diff --git a/src/main/java/com/stempo/api/global/config/SecurityConfig.java b/src/main/java/com/stempo/api/global/config/SecurityConfig.java new file mode 100644 index 0000000..2fab189 --- /dev/null +++ b/src/main/java/com/stempo/api/global/config/SecurityConfig.java @@ -0,0 +1,27 @@ +package com.stempo.api.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests.anyRequest().permitAll() + ); + return http.build(); + } +} From 9c4ee51c86c2702b44709a4707755c905b806583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 00:10:38 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat(User):=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/UserService.java | 7 +++ .../application/service/UserServiceImpl.java | 20 +++++++++ .../domain/repository/UserRepository.java | 7 +++ .../persistence/mappper/UserMapper.java | 23 ++++++++++ .../repository/UserJpaRepository.java | 7 +++ .../repository/UserRepositoryImpl.java | 21 +++++++++ .../domain/presentation/UserController.java | 31 +++++++++++++ .../dto/request/UserRegisterRequestDto.java | 30 +++++++++++++ .../api/global/common/dto/ApiResponse.java | 43 +++++++++++++++++++ 9 files changed, 189 insertions(+) create mode 100644 src/main/java/com/stempo/api/domain/application/service/UserService.java create mode 100644 src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java create mode 100644 src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java create mode 100644 src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java create mode 100644 src/main/java/com/stempo/api/domain/persistence/repository/UserJpaRepository.java create mode 100644 src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java create mode 100644 src/main/java/com/stempo/api/domain/presentation/UserController.java create mode 100644 src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java create mode 100644 src/main/java/com/stempo/api/global/common/dto/ApiResponse.java diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java new file mode 100644 index 0000000..bae371b --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -0,0 +1,7 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; + +public interface UserService { + String registerUser(UserRegisterRequestDto request); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java new file mode 100644 index 0000000..436f841 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -0,0 +1,20 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.domain.model.User; +import com.stempo.api.domain.domain.repository.UserRepository; +import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + @Override + public String registerUser(UserRegisterRequestDto requestDto) { + User user = UserRegisterRequestDto.toDomain(requestDto); + return userRepository.save(user).getId(); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java new file mode 100644 index 0000000..3702b77 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java @@ -0,0 +1,7 @@ +package com.stempo.api.domain.domain.repository; + +import com.stempo.api.domain.domain.model.User; + +public interface UserRepository { + User save(User user); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java b/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java new file mode 100644 index 0000000..7621486 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java @@ -0,0 +1,23 @@ +package com.stempo.api.domain.persistence.mappper; + +import com.stempo.api.domain.domain.model.User; +import com.stempo.api.domain.persistence.entity.UserEntity; + +public class UserMapper { + + public static UserEntity toEntity(User user) { + return UserEntity.builder() + .id(user.getId()) + .password(user.getPassword()) + .role(user.getRole()) + .build(); + } + + public static User toDomain(UserEntity entity) { + return User.builder() + .id(entity.getId()) + .password(entity.getPassword()) + .role(entity.getRole()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserJpaRepository.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserJpaRepository.java new file mode 100644 index 0000000..8968b55 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserJpaRepository.java @@ -0,0 +1,7 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.persistence.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..e2d8ee4 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java @@ -0,0 +1,21 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.domain.model.User; +import com.stempo.api.domain.domain.repository.UserRepository; +import com.stempo.api.domain.persistence.entity.UserEntity; +import com.stempo.api.domain.persistence.mappper.UserMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class UserRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + @Override + public User save(User user) { + UserEntity entity = userJpaRepository.save(UserMapper.toEntity(user)); + return UserMapper.toDomain(entity); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/UserController.java b/src/main/java/com/stempo/api/domain/presentation/UserController.java new file mode 100644 index 0000000..fad5f98 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/UserController.java @@ -0,0 +1,31 @@ +package com.stempo.api.domain.presentation; + +import com.stempo.api.domain.application.service.UserService; +import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; +import com.stempo.api.global.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +@Tag(name = "User", description = "회원") +public class UserController { + + private final UserService userService; + + @Operation(summary = "회원 가입", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") + @PostMapping("") + public ApiResponse registerUser( + @Valid @RequestBody UserRegisterRequestDto requestDto + ) { + String id = userService.registerUser(requestDto); + return ApiResponse.success(id); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java new file mode 100644 index 0000000..96f0729 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java @@ -0,0 +1,30 @@ +package com.stempo.api.domain.presentation.dto.request; + +import com.stempo.api.domain.domain.model.Role; +import com.stempo.api.domain.domain.model.User; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserRegisterRequestDto { + + @NotBlank + @Schema(description = "아이디", example = "test", minLength = 1, requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @NotNull + @Schema(description = "비밀번호", example = "1234", requiredMode = Schema.RequiredMode.REQUIRED) + private String password; + + public static User toDomain(UserRegisterRequestDto requestDto) { + return User.builder() + .id(requestDto.getId()) + .password(requestDto.getPassword()) + .role(Role.USER) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/global/common/dto/ApiResponse.java b/src/main/java/com/stempo/api/global/common/dto/ApiResponse.java new file mode 100644 index 0000000..4c18ef4 --- /dev/null +++ b/src/main/java/com/stempo/api/global/common/dto/ApiResponse.java @@ -0,0 +1,43 @@ +package com.stempo.api.global.common.dto; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ApiResponse { + + @Builder.Default + private Boolean success = true; + private T data; + + public static ApiResponse success() { + return ApiResponse. builder().build(); + } + + public static ApiResponse success(T data) { + return ApiResponse. builder() + .data(data) + .build(); + } + + public static ApiResponse failure() { + return ApiResponse. builder() + .success(false) + .build(); + } + + public static ApiResponse failure(T data) { + return ApiResponse. builder() + .success(false) + .data(data) + .build(); + } + + public String toJson() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} From caeb33291735c2578f20f4a0884f3c7ba8a8c5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 01:53:27 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat(Login):=20Redis=EC=97=90=20JWT=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RedisTokenService.java | 14 ++++++ .../service/RedisTokenServiceImpl.java | 34 +++++++++++++++ .../api/domain/domain/model/RedisToken.java | 43 +++++++++++++++++++ .../repository/RedisTokenRepository.java | 14 ++++++ .../repository/RedisTokenJpaRepository.java | 13 ++++++ .../repository/RedisTokenRepositoryImpl.java | 30 +++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 src/main/java/com/stempo/api/domain/application/service/RedisTokenService.java create mode 100644 src/main/java/com/stempo/api/domain/application/service/RedisTokenServiceImpl.java create mode 100644 src/main/java/com/stempo/api/domain/domain/model/RedisToken.java create mode 100644 src/main/java/com/stempo/api/domain/domain/repository/RedisTokenRepository.java create mode 100644 src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenJpaRepository.java create mode 100644 src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenRepositoryImpl.java diff --git a/src/main/java/com/stempo/api/domain/application/service/RedisTokenService.java b/src/main/java/com/stempo/api/domain/application/service/RedisTokenService.java new file mode 100644 index 0000000..4f4bc04 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/RedisTokenService.java @@ -0,0 +1,14 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.domain.model.RedisToken; +import com.stempo.api.domain.domain.model.Role; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; + +public interface RedisTokenService { + + RedisToken findByAccessToken(String token); + + RedisToken findByRefreshToken(String token); + + void saveToken(String id, Role role, TokenInfo tokenInfo); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/RedisTokenServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/RedisTokenServiceImpl.java new file mode 100644 index 0000000..a327f1d --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/RedisTokenServiceImpl.java @@ -0,0 +1,34 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.domain.model.RedisToken; +import com.stempo.api.domain.domain.model.Role; +import com.stempo.api.domain.domain.repository.RedisTokenRepository; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import com.stempo.api.global.auth.exception.TokenNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RedisTokenServiceImpl implements RedisTokenService { + + private final RedisTokenRepository redisTokenRepository; + + @Override + public RedisToken findByAccessToken(String token) { + return redisTokenRepository.findByAccessToken(token) + .orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다.")); + } + + @Override + public RedisToken findByRefreshToken(String token) { + return redisTokenRepository.findByRefreshToken(token) + .orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다.")); + } + + @Override + public void saveToken(String id, Role role, TokenInfo tokenInfo) { + RedisToken redisToken = RedisToken.create(id, role, tokenInfo); + redisTokenRepository.save(redisToken); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/model/RedisToken.java b/src/main/java/com/stempo/api/domain/domain/model/RedisToken.java new file mode 100644 index 0000000..02a0ac0 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/model/RedisToken.java @@ -0,0 +1,43 @@ +package com.stempo.api.domain.domain.model; + +import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RedisHash(value = "refresh", timeToLive = 60 * 60 * 24 * 14) +public class RedisToken { + + @Id + @Column(name = "user_id") + private String id; + + private Role role; + + @Indexed + private String accessToken; + + @Indexed + private String refreshToken; + + public static RedisToken create(String id, Role role, TokenInfo tokenInfo) { + return RedisToken.builder() + .id(id) + .role(role) + .accessToken(tokenInfo.getAccessToken()) + .refreshToken(tokenInfo.getRefreshToken()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/repository/RedisTokenRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/RedisTokenRepository.java new file mode 100644 index 0000000..f3442e6 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/repository/RedisTokenRepository.java @@ -0,0 +1,14 @@ +package com.stempo.api.domain.domain.repository; + +import com.stempo.api.domain.domain.model.RedisToken; + +import java.util.Optional; + +public interface RedisTokenRepository { + + Optional findByAccessToken(String token); + + Optional findByRefreshToken(String token); + + void save(RedisToken redisToken); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenJpaRepository.java b/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenJpaRepository.java new file mode 100644 index 0000000..aeb8b02 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenJpaRepository.java @@ -0,0 +1,13 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.domain.model.RedisToken; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RedisTokenJpaRepository extends CrudRepository { + + Optional findByAccessToken(String token); + + Optional findByRefreshToken(String token); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenRepositoryImpl.java new file mode 100644 index 0000000..9815ce3 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/RedisTokenRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.domain.model.RedisToken; +import com.stempo.api.domain.domain.repository.RedisTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class RedisTokenRepositoryImpl implements RedisTokenRepository { + + private final RedisTokenJpaRepository repository; + + @Override + public Optional findByAccessToken(String token) { + return repository.findByAccessToken(token); + } + + @Override + public Optional findByRefreshToken(String token) { + return repository.findByRefreshToken(token); + } + + @Override + public void save(RedisToken redisToken) { + repository.save(redisToken); + } +} From 348c68d6fd61754bb80384888fa010e09fc34d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 01:56:02 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat(Login):=20JWT=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- .../application/service/LoginService.java | 8 + .../application/service/LoginServiceImpl.java | 35 ++++ .../application/service/PasswordService.java | 5 + .../service/PasswordServiceImpl.java | 17 ++ .../application/service/UserService.java | 8 + .../application/service/UserServiceImpl.java | 17 ++ .../stempo/api/domain/domain/model/User.java | 26 ++- .../domain/repository/UserRepository.java | 4 + .../repository/UserRepositoryImpl.java | 14 +- .../domain/presentation/LoginController.java | 32 ++++ .../dto/request/LoginRequestDto.java | 20 +++ .../presentation/dto/response/TokenInfo.java | 22 +++ .../application/CustomUserDetailsService.java | 27 ++++ .../exception/TokenNotFoundException.java | 15 ++ .../exception/TokenValidateException.java | 15 ++ .../auth/filter/JwtAuthenticationFilter.java | 52 ++++++ .../api/global/auth/jwt/JwtTokenProvider.java | 150 ++++++++++++++++++ .../global/config/AuthenticationConfig.java | 28 ++++ .../global/config/PasswordEncoderConfig.java | 15 ++ .../api/global/config/SecurityConfig.java | 13 ++ .../global/exception/NotFoundException.java | 8 + .../stempo/api/global/util/ResponseUtil.java | 15 ++ 23 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/stempo/api/domain/application/service/LoginService.java create mode 100644 src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java create mode 100644 src/main/java/com/stempo/api/domain/application/service/PasswordService.java create mode 100644 src/main/java/com/stempo/api/domain/application/service/PasswordServiceImpl.java create mode 100644 src/main/java/com/stempo/api/domain/presentation/LoginController.java create mode 100644 src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java create mode 100644 src/main/java/com/stempo/api/domain/presentation/dto/response/TokenInfo.java create mode 100644 src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java create mode 100644 src/main/java/com/stempo/api/global/auth/exception/TokenNotFoundException.java create mode 100644 src/main/java/com/stempo/api/global/auth/exception/TokenValidateException.java create mode 100644 src/main/java/com/stempo/api/global/auth/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/stempo/api/global/auth/jwt/JwtTokenProvider.java create mode 100644 src/main/java/com/stempo/api/global/config/AuthenticationConfig.java create mode 100644 src/main/java/com/stempo/api/global/config/PasswordEncoderConfig.java create mode 100644 src/main/java/com/stempo/api/global/exception/NotFoundException.java create mode 100644 src/main/java/com/stempo/api/global/util/ResponseUtil.java diff --git a/build.gradle b/build.gradle index ed2acb2..16a927f 100644 --- a/build.gradle +++ b/build.gradle @@ -25,12 +25,13 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' // JWT 라이브러리 - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' // JWT 구현체 - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JWT Jackson 모듈 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' // JWT 라이브러리 + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' // JWT 구현체 + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' // JWT Jackson 모듈 // DB implementation 'org.mariadb.jdbc:mariadb-java-client:3.4.1' // MariaDB JDBC Driver + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA implementation 'org.springframework.boot:spring-boot-starter-validation' // Hibernate Validator implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' // Bean Validation diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginService.java b/src/main/java/com/stempo/api/domain/application/service/LoginService.java new file mode 100644 index 0000000..4691b78 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/LoginService.java @@ -0,0 +1,8 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; + +public interface LoginService { + TokenInfo login(LoginRequestDto requestDto); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java new file mode 100644 index 0000000..11cc9ea --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java @@ -0,0 +1,35 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.domain.model.User; +import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import com.stempo.api.global.auth.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoginServiceImpl implements LoginService { + + private final UserService userService; + private final RedisTokenService redisTokenService; + private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public TokenInfo login(LoginRequestDto requestDto) { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(requestDto.getId(), requestDto.getPassword()); + authenticationManager.authenticate(authenticationToken); + User loginUser = userService.findByIdOrThrow(requestDto.getId()); + return generateAndSaveToken(loginUser); + } + + private TokenInfo generateAndSaveToken(User loginUser) { + TokenInfo tokenInfo = jwtTokenProvider.generateToken(loginUser.getId(), loginUser.getRole()); + redisTokenService.saveToken(loginUser.getId(), loginUser.getRole(), tokenInfo); + return tokenInfo; + } +} diff --git a/src/main/java/com/stempo/api/domain/application/service/PasswordService.java b/src/main/java/com/stempo/api/domain/application/service/PasswordService.java new file mode 100644 index 0000000..8aa0f32 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/PasswordService.java @@ -0,0 +1,5 @@ +package com.stempo.api.domain.application.service; + +public interface PasswordService { + String encodePassword(String password); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/PasswordServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/PasswordServiceImpl.java new file mode 100644 index 0000000..4d15963 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/PasswordServiceImpl.java @@ -0,0 +1,17 @@ +package com.stempo.api.domain.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PasswordServiceImpl implements PasswordService { + + private final PasswordEncoder passwordEncoder; + + @Override + public String encodePassword(String rawPassword) { + return passwordEncoder.encode(rawPassword); + } +} diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java index bae371b..c2224a6 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserService.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -1,7 +1,15 @@ package com.stempo.api.domain.application.service; +import com.stempo.api.domain.domain.model.User; import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; +import java.util.Optional; + public interface UserService { + String registerUser(UserRegisterRequestDto request); + + Optional findById(String id); + + User findByIdOrThrow(String id); } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index 436f841..84b2fe4 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -3,18 +3,35 @@ import com.stempo.api.domain.domain.model.User; import com.stempo.api.domain.domain.repository.UserRepository; import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; +import com.stempo.api.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; + private final PasswordService passwordService; @Override public String registerUser(UserRegisterRequestDto requestDto) { User user = UserRegisterRequestDto.toDomain(requestDto); + String encodedPassword = passwordService.encodePassword(user.getPassword()); + user.updatePassword(encodedPassword); return userRepository.save(user).getId(); } + + @Override + public Optional findById(String id) { + return userRepository.findById(id); + } + + @Override + public User findByIdOrThrow(String id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("[User] id: " + id + " not found")); + } } diff --git a/src/main/java/com/stempo/api/domain/domain/model/User.java b/src/main/java/com/stempo/api/domain/domain/model/User.java index a6ccdd7..63dbf07 100644 --- a/src/main/java/com/stempo/api/domain/domain/model/User.java +++ b/src/main/java/com/stempo/api/domain/domain/model/User.java @@ -6,15 +6,39 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; @Getter @Setter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class User { +public class User implements UserDetails { private String id; private String password; private Role role; + + public static User createUserDetails(User user) { + return new User(user.getId(), user.getPassword(), user.getRole()); + } + + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority(getRole().getKey())); + } + + @Override + public String getUsername() { + return id; + } + + public void updatePassword(String encodedPassword) { + setPassword(encodedPassword); + } } diff --git a/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java index 3702b77..55578c4 100644 --- a/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java +++ b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java @@ -2,6 +2,10 @@ import com.stempo.api.domain.domain.model.User; +import java.util.Optional; + public interface UserRepository { User save(User user); + + Optional findById(String id); } diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java index e2d8ee4..72b7632 100644 --- a/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java @@ -7,15 +7,23 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -@RequiredArgsConstructor +import java.util.Optional; + @Repository +@RequiredArgsConstructor public class UserRepositoryImpl implements UserRepository { - private final UserJpaRepository userJpaRepository; + private final UserJpaRepository repository; @Override public User save(User user) { - UserEntity entity = userJpaRepository.save(UserMapper.toEntity(user)); + UserEntity entity = repository.save(UserMapper.toEntity(user)); return UserMapper.toDomain(entity); } + + @Override + public Optional findById(String id) { + return repository.findById(id) + .map(UserMapper::toDomain); + } } diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java new file mode 100644 index 0000000..1d7018c --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -0,0 +1,32 @@ +package com.stempo.api.domain.presentation; + +import com.stempo.api.domain.application.service.LoginService; +import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import com.stempo.api.global.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/login") +@RequiredArgsConstructor +@Tag(name = "Login", description = "로그인") +public class LoginController { + + private final LoginService loginService; + + @Operation(summary = "로그인", description = "ROLE_ANONYMOUS 이상의권한이 필요함") + @PostMapping("") + public ApiResponse login( + @Valid @RequestBody LoginRequestDto requestDto + ) { + TokenInfo token = loginService.login(requestDto); + return ApiResponse.success(token); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java new file mode 100644 index 0000000..2c4d69c --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java @@ -0,0 +1,20 @@ +package com.stempo.api.domain.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequestDto { + + @NotBlank + @Schema(description = "아이디", example = "test", minLength = 1, requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @NotNull + @Schema(description = "비밀번호", example = "1234", requiredMode = Schema.RequiredMode.REQUIRED) + private String password; +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/response/TokenInfo.java b/src/main/java/com/stempo/api/domain/presentation/dto/response/TokenInfo.java new file mode 100644 index 0000000..a0d3f4b --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/response/TokenInfo.java @@ -0,0 +1,22 @@ +package com.stempo.api.domain.presentation.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TokenInfo { + + private String accessToken; + private String refreshToken; + + public static TokenInfo create(String accessToken, String refreshToken) { + return TokenInfo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java b/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java new file mode 100644 index 0000000..e6860df --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.stempo.api.global.auth.application; + +import com.stempo.api.domain.application.service.UserService; +import com.stempo.api.domain.domain.model.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserService userService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userService.findById(username) + .map(this::createUserDetails) + .orElseThrow(() -> new UsernameNotFoundException("[User] username: " + username + " not found")); + } + + private UserDetails createUserDetails(User user) { + return User.createUserDetails(user); + } +} diff --git a/src/main/java/com/stempo/api/global/auth/exception/TokenNotFoundException.java b/src/main/java/com/stempo/api/global/auth/exception/TokenNotFoundException.java new file mode 100644 index 0000000..ec8043b --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/exception/TokenNotFoundException.java @@ -0,0 +1,15 @@ +package com.stempo.api.global.auth.exception; + +public class TokenNotFoundException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "토큰이 존재하지 않습니다."; + + public TokenNotFoundException() { + super(DEFAULT_MESSAGE); + } + + public TokenNotFoundException(String s) { + super(s); + } + +} diff --git a/src/main/java/com/stempo/api/global/auth/exception/TokenValidateException.java b/src/main/java/com/stempo/api/global/auth/exception/TokenValidateException.java new file mode 100644 index 0000000..24a3d0c --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/exception/TokenValidateException.java @@ -0,0 +1,15 @@ +package com.stempo.api.global.auth.exception; + +public class TokenValidateException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "토큰이 유효하지 않습니다."; + + public TokenValidateException() { + super(DEFAULT_MESSAGE); + } + + public TokenValidateException(String s) { + super(s); + } + +} diff --git a/src/main/java/com/stempo/api/global/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/stempo/api/global/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..345b19d --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,52 @@ +package com.stempo.api.global.auth.filter; + +import com.stempo.api.domain.application.service.RedisTokenService; +import com.stempo.api.domain.domain.model.RedisToken; +import com.stempo.api.global.auth.jwt.JwtTokenProvider; +import com.stempo.api.global.util.ResponseUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final RedisTokenService redisTokenService; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + if (!authenticateToken(httpServletRequest, httpServletResponse)) { + return; + } + chain.doFilter(request, response); + } + + private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response) throws IOException { + String token = jwtTokenProvider.resolveToken(request); + if (token != null && jwtTokenProvider.validateToken(token)) { + RedisToken redisToken = jwtTokenProvider.isRefreshToken(token) ? redisTokenService.findByRefreshToken(token) : redisTokenService.findByAccessToken(token); + if (redisToken == null) { + log.warn("Token not found in redis"); + ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + return true; + } +} diff --git a/src/main/java/com/stempo/api/global/auth/jwt/JwtTokenProvider.java b/src/main/java/com/stempo/api/global/auth/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..4a3af90 --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/jwt/JwtTokenProvider.java @@ -0,0 +1,150 @@ +package com.stempo.api.global.auth.jwt; + +import com.stempo.api.domain.domain.model.Role; +import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import com.stempo.api.global.auth.exception.TokenValidateException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; + +@Component +@Slf4j +public class JwtTokenProvider { + + private final Key key; + private final long accessTokenDuration; + private final long refreshTokenDuration; + + public JwtTokenProvider( + @Value("${security.jwt.secret-key}") String secretKey, + @Value("${security.jwt.token-validity-in-seconds.access-token}") long accessTokenDuration, + @Value("${security.jwt.token-validity-in-seconds.refresh-token}") long refreshTokenDuration + ) { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + this.accessTokenDuration = accessTokenDuration; + this.refreshTokenDuration = refreshTokenDuration; + } + + public TokenInfo generateToken(String id, Role role) { + Date expiry = new Date(); + Date accessTokenExpiry = new Date(expiry.getTime() + (accessTokenDuration)); + String accessToken = Jwts.builder() + .setSubject(id) + .claim("role", role) + .setIssuedAt(expiry) + .setExpiration(accessTokenExpiry) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + Date refreshTokenExpiry = new Date(expiry.getTime() + (refreshTokenDuration)); + String refreshToken = Jwts.builder() + .setSubject(id) + .claim("role", role) + .setIssuedAt(expiry) + .setExpiration(refreshTokenExpiry) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return TokenInfo.create(accessToken, refreshToken); + } + + public boolean isRefreshToken(String token) { + try { + Claims claims = parseClaims(token); + Date issuedAt = claims.getIssuedAt(); + Date expiration = claims.getExpiration(); + if (issuedAt != null && expiration != null) { + long duration = expiration.getTime() - issuedAt.getTime(); + return duration == refreshTokenDuration; + } + } catch (Exception e) { + log.debug("Failed to check if the token is a refresh token", e); + } + return false; + } + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + log.debug("claims : {}", claims); + log.debug("accessToken : {}", accessToken); + + if (claims.get("role") == null) { + throw new TokenValidateException("권한 정보가 없는 토큰입니다."); + } + + Collection authorities = + Arrays.stream(claims.get("role").toString().split(",")) + .map(this::formatRoleString) + .map(SimpleGrantedAuthority::new) + .toList(); + + UserDetails principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token"); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token"); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token"); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty."); + } + return false; + } + + public Claims parseClaims(String accessToken) { + try { + return Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + private String formatRoleString(String role) { + if (!role.startsWith("ROLE_")) { + return "ROLE_" + role; + } + return role; + } +} diff --git a/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java b/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java new file mode 100644 index 0000000..4d2393c --- /dev/null +++ b/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java @@ -0,0 +1,28 @@ +package com.stempo.api.global.config; + +import com.stempo.api.global.auth.application.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class AuthenticationConfig { + + private final CustomUserDetailsService customUserDetailsService; + private final PasswordEncoder passwordEncoder; + + @Bean + public AuthenticationManager authenticationManager() { + DaoAuthenticationProvider loginProvider = new DaoAuthenticationProvider(); + loginProvider.setUserDetailsService(customUserDetailsService); + loginProvider.setPasswordEncoder(passwordEncoder); + return new ProviderManager(List.of(loginProvider)); + } +} diff --git a/src/main/java/com/stempo/api/global/config/PasswordEncoderConfig.java b/src/main/java/com/stempo/api/global/config/PasswordEncoderConfig.java new file mode 100644 index 0000000..140a2a0 --- /dev/null +++ b/src/main/java/com/stempo/api/global/config/PasswordEncoderConfig.java @@ -0,0 +1,15 @@ +package com.stempo.api.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/stempo/api/global/config/SecurityConfig.java b/src/main/java/com/stempo/api/global/config/SecurityConfig.java index 2fab189..866291d 100644 --- a/src/main/java/com/stempo/api/global/config/SecurityConfig.java +++ b/src/main/java/com/stempo/api/global/config/SecurityConfig.java @@ -1,5 +1,9 @@ package com.stempo.api.global.config; +import com.stempo.api.domain.application.service.RedisTokenService; +import com.stempo.api.global.auth.filter.JwtAuthenticationFilter; +import com.stempo.api.global.auth.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -7,11 +11,16 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final RedisTokenService redisTokenService; + private final JwtTokenProvider jwtTokenProvider; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -21,6 +30,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().permitAll() + ) + .addFilterBefore( + new JwtAuthenticationFilter(redisTokenService, jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class ); return http.build(); } diff --git a/src/main/java/com/stempo/api/global/exception/NotFoundException.java b/src/main/java/com/stempo/api/global/exception/NotFoundException.java new file mode 100644 index 0000000..b89d3ab --- /dev/null +++ b/src/main/java/com/stempo/api/global/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package com.stempo.api.global.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/stempo/api/global/util/ResponseUtil.java b/src/main/java/com/stempo/api/global/util/ResponseUtil.java new file mode 100644 index 0000000..58cb6de --- /dev/null +++ b/src/main/java/com/stempo/api/global/util/ResponseUtil.java @@ -0,0 +1,15 @@ +package com.stempo.api.global.util; + +import com.stempo.api.global.common.dto.ApiResponse; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class ResponseUtil { + + public static void sendErrorResponse(HttpServletResponse response, int status) throws IOException { + response.getWriter().write(ApiResponse.failure().toJson()); + response.setContentType("application/json"); + response.setStatus(status); + } +} From 4790418bb991cb58f4987dd369e0458e634d0693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 02:18:44 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat(Login):=20JWT=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/LoginService.java | 4 ++++ .../application/service/LoginServiceImpl.java | 24 +++++++++++++++++++ .../application/service/UserService.java | 2 ++ .../application/service/UserServiceImpl.java | 5 ++++ .../domain/repository/UserRepository.java | 3 +++ .../repository/UserRepositoryImpl.java | 5 ++++ .../domain/presentation/LoginController.java | 14 ++++++++--- .../domain/presentation/UserController.java | 4 +--- .../auth/exception/TokenForgeryException.java | 15 ++++++++++++ 9 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/stempo/api/global/auth/exception/TokenForgeryException.java diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginService.java b/src/main/java/com/stempo/api/domain/application/service/LoginService.java index 4691b78..41cd7a3 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginService.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginService.java @@ -2,7 +2,11 @@ import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import jakarta.servlet.http.HttpServletRequest; public interface LoginService { + TokenInfo login(LoginRequestDto requestDto); + + TokenInfo reissueToken(HttpServletRequest request); } diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java index 11cc9ea..b5046c6 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java @@ -1,12 +1,16 @@ package com.stempo.api.domain.application.service; +import com.stempo.api.domain.domain.model.RedisToken; import com.stempo.api.domain.domain.model.User; import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; import com.stempo.api.domain.presentation.dto.response.TokenInfo; +import com.stempo.api.global.auth.exception.TokenForgeryException; import com.stempo.api.global.auth.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; @Service @@ -27,9 +31,29 @@ public TokenInfo login(LoginRequestDto requestDto) { return generateAndSaveToken(loginUser); } + @Override + public TokenInfo reissueToken(HttpServletRequest request) { + String refreshToken = jwtTokenProvider.resolveToken(request); + Authentication authentication = jwtTokenProvider.getAuthentication(refreshToken); + RedisToken redisToken = redisTokenService.findByRefreshToken(refreshToken); + + validateUserExistence(authentication); + + TokenInfo newTokenInfo = jwtTokenProvider.generateToken(redisToken.getId(), redisToken.getRole()); + redisTokenService.saveToken(redisToken.getId(), redisToken.getRole(), newTokenInfo); + return newTokenInfo; + } + private TokenInfo generateAndSaveToken(User loginUser) { TokenInfo tokenInfo = jwtTokenProvider.generateToken(loginUser.getId(), loginUser.getRole()); redisTokenService.saveToken(loginUser.getId(), loginUser.getRole(), tokenInfo); return tokenInfo; } + + private void validateUserExistence(Authentication authentication) { + String id = authentication.getName(); + if (!userService.existsById(id)) { + throw new TokenForgeryException("Non-existent user token."); + } + } } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java index c2224a6..86acf7e 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserService.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -12,4 +12,6 @@ public interface UserService { Optional findById(String id); User findByIdOrThrow(String id); + + boolean existsById(String id); } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index 84b2fe4..7264149 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -34,4 +34,9 @@ public User findByIdOrThrow(String id) { return userRepository.findById(id) .orElseThrow(() -> new NotFoundException("[User] id: " + id + " not found")); } + + @Override + public boolean existsById(String id) { + return userRepository.existsById(id); + } } diff --git a/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java index 55578c4..e43821a 100644 --- a/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java +++ b/src/main/java/com/stempo/api/domain/domain/repository/UserRepository.java @@ -5,7 +5,10 @@ import java.util.Optional; public interface UserRepository { + User save(User user); Optional findById(String id); + + boolean existsById(String id); } diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java index 72b7632..9d2cd9c 100644 --- a/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserRepositoryImpl.java @@ -26,4 +26,9 @@ public Optional findById(String id) { return repository.findById(id) .map(UserMapper::toDomain); } + + @Override + public boolean existsById(String id) { + return repository.existsById(id); + } } diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java index 1d7018c..5a4bfc5 100644 --- a/src/main/java/com/stempo/api/domain/presentation/LoginController.java +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -6,15 +6,14 @@ import com.stempo.api.global.common.dto.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/login") @RequiredArgsConstructor @Tag(name = "Login", description = "로그인") public class LoginController { @@ -22,11 +21,20 @@ public class LoginController { private final LoginService loginService; @Operation(summary = "로그인", description = "ROLE_ANONYMOUS 이상의권한이 필요함") - @PostMapping("") + @PostMapping("/api/vi/login") public ApiResponse login( @Valid @RequestBody LoginRequestDto requestDto ) { TokenInfo token = loginService.login(requestDto); return ApiResponse.success(token); } + + @Operation(summary = "토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") + @PostMapping("/api/vi/reissue") + public ApiResponse reissueToken( + HttpServletRequest request + ) { + TokenInfo token = loginService.reissueToken(request); + return ApiResponse.success(token); + } } diff --git a/src/main/java/com/stempo/api/domain/presentation/UserController.java b/src/main/java/com/stempo/api/domain/presentation/UserController.java index fad5f98..6155820 100644 --- a/src/main/java/com/stempo/api/domain/presentation/UserController.java +++ b/src/main/java/com/stempo/api/domain/presentation/UserController.java @@ -9,11 +9,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/users") @RequiredArgsConstructor @Tag(name = "User", description = "회원") public class UserController { @@ -21,7 +19,7 @@ public class UserController { private final UserService userService; @Operation(summary = "회원 가입", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") - @PostMapping("") + @PostMapping("/api/v1/users") public ApiResponse registerUser( @Valid @RequestBody UserRegisterRequestDto requestDto ) { diff --git a/src/main/java/com/stempo/api/global/auth/exception/TokenForgeryException.java b/src/main/java/com/stempo/api/global/auth/exception/TokenForgeryException.java new file mode 100644 index 0000000..e78239c --- /dev/null +++ b/src/main/java/com/stempo/api/global/auth/exception/TokenForgeryException.java @@ -0,0 +1,15 @@ +package com.stempo.api.global.auth.exception; + +public class TokenForgeryException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "토큰이 변조되었습니다."; + + public TokenForgeryException() { + super(DEFAULT_MESSAGE); + } + + public TokenForgeryException(String s) { + super(s); + } + +} From 742d93c745f6f4b36c3b292106769b37cbe3eb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 16:24:33 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor(Login):=20Device=20Tag=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=9D=B4=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/application/service/LoginService.java | 3 +-- .../application/service/LoginServiceImpl.java | 14 ++++---------- .../domain/application/service/UserService.java | 2 ++ .../application/service/UserServiceImpl.java | 8 ++++++++ .../com/stempo/api/domain/domain/model/User.java | 4 ++++ .../api/domain/presentation/LoginController.java | 14 ++++++++------ 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginService.java b/src/main/java/com/stempo/api/domain/application/service/LoginService.java index 41cd7a3..b5e7b28 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginService.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginService.java @@ -1,12 +1,11 @@ package com.stempo.api.domain.application.service; -import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; import com.stempo.api.domain.presentation.dto.response.TokenInfo; import jakarta.servlet.http.HttpServletRequest; public interface LoginService { - TokenInfo login(LoginRequestDto requestDto); + TokenInfo loginOrRegister(String deviceTag); TokenInfo reissueToken(HttpServletRequest request); } diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java index b5046c6..0253257 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java @@ -2,14 +2,11 @@ import com.stempo.api.domain.domain.model.RedisToken; import com.stempo.api.domain.domain.model.User; -import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; import com.stempo.api.domain.presentation.dto.response.TokenInfo; import com.stempo.api.global.auth.exception.TokenForgeryException; import com.stempo.api.global.auth.jwt.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; @@ -19,16 +16,13 @@ public class LoginServiceImpl implements LoginService { private final UserService userService; private final RedisTokenService redisTokenService; - private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; @Override - public TokenInfo login(LoginRequestDto requestDto) { - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(requestDto.getId(), requestDto.getPassword()); - authenticationManager.authenticate(authenticationToken); - User loginUser = userService.findByIdOrThrow(requestDto.getId()); - return generateAndSaveToken(loginUser); + public TokenInfo loginOrRegister(String deviceTag) { + User user = userService.findById(deviceTag) + .orElseGet(() -> userService.registerUser(deviceTag)); + return generateAndSaveToken(user); } @Override diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java index 86acf7e..fc92b3d 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserService.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -9,6 +9,8 @@ public interface UserService { String registerUser(UserRegisterRequestDto request); + User registerUser(String deviceTag); + Optional findById(String id); User findByIdOrThrow(String id); diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index 7264149..aa5316b 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -24,6 +24,14 @@ public String registerUser(UserRegisterRequestDto requestDto) { return userRepository.save(user).getId(); } + @Override + public User registerUser(String deviceTag) { + User user = User.create(deviceTag); + String encodedPassword = passwordService.encodePassword(user.getPassword()); + user.updatePassword(encodedPassword); + return userRepository.save(user); + } + @Override public Optional findById(String id) { return userRepository.findById(id); diff --git a/src/main/java/com/stempo/api/domain/domain/model/User.java b/src/main/java/com/stempo/api/domain/domain/model/User.java index 63dbf07..1af1509 100644 --- a/src/main/java/com/stempo/api/domain/domain/model/User.java +++ b/src/main/java/com/stempo/api/domain/domain/model/User.java @@ -28,6 +28,10 @@ public static User createUserDetails(User user) { return new User(user.getId(), user.getPassword(), user.getRole()); } + public static User create(String deviceTag) { + return new User(deviceTag, "", Role.USER); + } + @Override public Collection getAuthorities() { return Collections.singletonList(new SimpleGrantedAuthority(getRole().getKey())); diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java index 5a4bfc5..7d6d054 100644 --- a/src/main/java/com/stempo/api/domain/presentation/LoginController.java +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -1,16 +1,15 @@ package com.stempo.api.domain.presentation; import com.stempo.api.domain.application.service.LoginService; -import com.stempo.api.domain.presentation.dto.request.LoginRequestDto; import com.stempo.api.domain.presentation.dto.response.TokenInfo; import com.stempo.api.global.common.dto.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController @@ -23,18 +22,21 @@ public class LoginController { @Operation(summary = "로그인", description = "ROLE_ANONYMOUS 이상의권한이 필요함") @PostMapping("/api/vi/login") public ApiResponse login( - @Valid @RequestBody LoginRequestDto requestDto + @RequestHeader("Device-Tag") String deviceTag ) { - TokenInfo token = loginService.login(requestDto); + TokenInfo token = loginService.loginOrRegister(deviceTag); return ApiResponse.success(token); } @Operation(summary = "토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") @PostMapping("/api/vi/reissue") public ApiResponse reissueToken( - HttpServletRequest request + HttpServletRequest request, + HttpServletResponse response ) { TokenInfo token = loginService.reissueToken(request); + response.setHeader("Authorization", "Bearer" + token.getAccessToken()); + response.setHeader("Refresh-Token", token.getRefreshToken()); return ApiResponse.success(token); } } From 5a20c548be232ebe95055e7e1f23fa0f8c931f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 16:37:22 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor(User):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/presentation/UserController.java | 29 ------------------- .../application/CustomUserDetailsService.java | 27 ----------------- .../global/config/AuthenticationConfig.java | 28 ------------------ 3 files changed, 84 deletions(-) delete mode 100644 src/main/java/com/stempo/api/domain/presentation/UserController.java delete mode 100644 src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java delete mode 100644 src/main/java/com/stempo/api/global/config/AuthenticationConfig.java diff --git a/src/main/java/com/stempo/api/domain/presentation/UserController.java b/src/main/java/com/stempo/api/domain/presentation/UserController.java deleted file mode 100644 index 6155820..0000000 --- a/src/main/java/com/stempo/api/domain/presentation/UserController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.stempo.api.domain.presentation; - -import com.stempo.api.domain.application.service.UserService; -import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; -import com.stempo.api.global.common.dto.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@Tag(name = "User", description = "회원") -public class UserController { - - private final UserService userService; - - @Operation(summary = "회원 가입", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") - @PostMapping("/api/v1/users") - public ApiResponse registerUser( - @Valid @RequestBody UserRegisterRequestDto requestDto - ) { - String id = userService.registerUser(requestDto); - return ApiResponse.success(id); - } -} diff --git a/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java b/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java deleted file mode 100644 index e6860df..0000000 --- a/src/main/java/com/stempo/api/global/auth/application/CustomUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.stempo.api.global.auth.application; - -import com.stempo.api.domain.application.service.UserService; -import com.stempo.api.domain.domain.model.User; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - - private final UserService userService; - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return userService.findById(username) - .map(this::createUserDetails) - .orElseThrow(() -> new UsernameNotFoundException("[User] username: " + username + " not found")); - } - - private UserDetails createUserDetails(User user) { - return User.createUserDetails(user); - } -} diff --git a/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java b/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java deleted file mode 100644 index 4d2393c..0000000 --- a/src/main/java/com/stempo/api/global/config/AuthenticationConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.stempo.api.global.config; - -import com.stempo.api.global.auth.application.CustomUserDetailsService; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.List; - -@Configuration -@RequiredArgsConstructor -public class AuthenticationConfig { - - private final CustomUserDetailsService customUserDetailsService; - private final PasswordEncoder passwordEncoder; - - @Bean - public AuthenticationManager authenticationManager() { - DaoAuthenticationProvider loginProvider = new DaoAuthenticationProvider(); - loginProvider.setUserDetailsService(customUserDetailsService); - loginProvider.setPasswordEncoder(passwordEncoder); - return new ProviderManager(List.of(loginProvider)); - } -} From 018ea74bda266a394cf01a16c5aa99352d830e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 16:46:21 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor(Login):=20User=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=BB=AC=EB=9F=BC=EB=AA=85(id->device=5Ft?= =?UTF-8?q?ag)=20=EB=B3=80=EA=B2=BD,=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20UUID=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EB=9E=9C=EB=8D=A4=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/LoginServiceImpl.java | 4 +-- .../application/service/UserService.java | 5 ---- .../application/service/UserServiceImpl.java | 20 ++----------- .../stempo/api/domain/domain/model/User.java | 28 +++-------------- .../domain/persistence/entity/UserEntity.java | 2 +- .../persistence/mappper/UserMapper.java | 4 +-- .../domain/presentation/LoginController.java | 10 ++++--- .../dto/request/LoginRequestDto.java | 20 ------------- .../dto/request/UserRegisterRequestDto.java | 30 ------------------- 9 files changed, 18 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java delete mode 100644 src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java index 0253257..525a5a5 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java @@ -39,8 +39,8 @@ public TokenInfo reissueToken(HttpServletRequest request) { } private TokenInfo generateAndSaveToken(User loginUser) { - TokenInfo tokenInfo = jwtTokenProvider.generateToken(loginUser.getId(), loginUser.getRole()); - redisTokenService.saveToken(loginUser.getId(), loginUser.getRole(), tokenInfo); + TokenInfo tokenInfo = jwtTokenProvider.generateToken(loginUser.getDeviceTag(), loginUser.getRole()); + redisTokenService.saveToken(loginUser.getDeviceTag(), loginUser.getRole(), tokenInfo); return tokenInfo; } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java index fc92b3d..9687e3a 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserService.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -1,19 +1,14 @@ package com.stempo.api.domain.application.service; import com.stempo.api.domain.domain.model.User; -import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; import java.util.Optional; public interface UserService { - String registerUser(UserRegisterRequestDto request); - User registerUser(String deviceTag); Optional findById(String id); - User findByIdOrThrow(String id); - boolean existsById(String id); } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index aa5316b..5b7483b 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -2,12 +2,11 @@ import com.stempo.api.domain.domain.model.User; import com.stempo.api.domain.domain.repository.UserRepository; -import com.stempo.api.domain.presentation.dto.request.UserRegisterRequestDto; -import com.stempo.api.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.Optional; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -16,17 +15,10 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final PasswordService passwordService; - @Override - public String registerUser(UserRegisterRequestDto requestDto) { - User user = UserRegisterRequestDto.toDomain(requestDto); - String encodedPassword = passwordService.encodePassword(user.getPassword()); - user.updatePassword(encodedPassword); - return userRepository.save(user).getId(); - } - @Override public User registerUser(String deviceTag) { - User user = User.create(deviceTag); + String rawPassword = UUID.randomUUID().toString(); + User user = User.create(deviceTag, rawPassword); String encodedPassword = passwordService.encodePassword(user.getPassword()); user.updatePassword(encodedPassword); return userRepository.save(user); @@ -37,12 +29,6 @@ public Optional findById(String id) { return userRepository.findById(id); } - @Override - public User findByIdOrThrow(String id) { - return userRepository.findById(id) - .orElseThrow(() -> new NotFoundException("[User] id: " + id + " not found")); - } - @Override public boolean existsById(String id) { return userRepository.existsById(id); diff --git a/src/main/java/com/stempo/api/domain/domain/model/User.java b/src/main/java/com/stempo/api/domain/domain/model/User.java index 1af1509..b728d04 100644 --- a/src/main/java/com/stempo/api/domain/domain/model/User.java +++ b/src/main/java/com/stempo/api/domain/domain/model/User.java @@ -6,40 +6,20 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; @Getter @Setter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class User implements UserDetails { +public class User { - private String id; + private String deviceTag; private String password; private Role role; - public static User createUserDetails(User user) { - return new User(user.getId(), user.getPassword(), user.getRole()); - } - - public static User create(String deviceTag) { - return new User(deviceTag, "", Role.USER); - } - - @Override - public Collection getAuthorities() { - return Collections.singletonList(new SimpleGrantedAuthority(getRole().getKey())); - } - - @Override - public String getUsername() { - return id; + public static User create(String deviceTag, String password) { + return new User(deviceTag, password, Role.USER); } public void updatePassword(String encodedPassword) { diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java index 8a2b387..e9d7dbf 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java @@ -24,7 +24,7 @@ public class UserEntity extends BaseEntity { @Id - private String id; + private String deviceTag; @Column(nullable = false) private String password; diff --git a/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java b/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java index 7621486..8954244 100644 --- a/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java +++ b/src/main/java/com/stempo/api/domain/persistence/mappper/UserMapper.java @@ -7,7 +7,7 @@ public class UserMapper { public static UserEntity toEntity(User user) { return UserEntity.builder() - .id(user.getId()) + .deviceTag(user.getDeviceTag()) .password(user.getPassword()) .role(user.getRole()) .build(); @@ -15,7 +15,7 @@ public static UserEntity toEntity(User user) { public static User toDomain(UserEntity entity) { return User.builder() - .id(entity.getId()) + .deviceTag(entity.getDeviceTag()) .password(entity.getPassword()) .role(entity.getRole()) .build(); diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java index 7d6d054..428a17d 100644 --- a/src/main/java/com/stempo/api/domain/presentation/LoginController.java +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -19,16 +19,18 @@ public class LoginController { private final LoginService loginService; - @Operation(summary = "로그인", description = "ROLE_ANONYMOUS 이상의권한이 필요함") + @Operation(summary = "로그인", description = "ROLE_ANONYMOUS 이상의권한이 필요함
" + + "일반 계정일 경우 Device-Tag만 기입하면 됨") @PostMapping("/api/vi/login") public ApiResponse login( - @RequestHeader("Device-Tag") String deviceTag + @RequestHeader(value = "Device-Tag", defaultValue = "490154203237518") String deviceTag, + @RequestHeader(value = "Password", required = false) String password ) { TokenInfo token = loginService.loginOrRegister(deviceTag); return ApiResponse.success(token); } - @Operation(summary = "토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") + @Operation(summary = "[U] 토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") @PostMapping("/api/vi/reissue") public ApiResponse reissueToken( HttpServletRequest request, @@ -37,6 +39,6 @@ public ApiResponse reissueToken( TokenInfo token = loginService.reissueToken(request); response.setHeader("Authorization", "Bearer" + token.getAccessToken()); response.setHeader("Refresh-Token", token.getRefreshToken()); - return ApiResponse.success(token); + return ApiResponse.success(); } } diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java deleted file mode 100644 index 2c4d69c..0000000 --- a/src/main/java/com/stempo/api/domain/presentation/dto/request/LoginRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.stempo.api.domain.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class LoginRequestDto { - - @NotBlank - @Schema(description = "아이디", example = "test", minLength = 1, requiredMode = Schema.RequiredMode.REQUIRED) - private String id; - - @NotNull - @Schema(description = "비밀번호", example = "1234", requiredMode = Schema.RequiredMode.REQUIRED) - private String password; -} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java deleted file mode 100644 index 96f0729..0000000 --- a/src/main/java/com/stempo/api/domain/presentation/dto/request/UserRegisterRequestDto.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.stempo.api.domain.presentation.dto.request; - -import com.stempo.api.domain.domain.model.Role; -import com.stempo.api.domain.domain.model.User; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UserRegisterRequestDto { - - @NotBlank - @Schema(description = "아이디", example = "test", minLength = 1, requiredMode = Schema.RequiredMode.REQUIRED) - private String id; - - @NotNull - @Schema(description = "비밀번호", example = "1234", requiredMode = Schema.RequiredMode.REQUIRED) - private String password; - - public static User toDomain(UserRegisterRequestDto requestDto) { - return User.builder() - .id(requestDto.getId()) - .password(requestDto.getPassword()) - .role(Role.USER) - .build(); - } -} From ca6c1df7a292490d70f6dacf0b2bbdcecce8ec8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 16:51:28 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor(Login):=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=ED=97=A4=EB=8D=94=EC=97=90=20=EB=8B=B4=EC=95=84=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stempo/api/domain/presentation/LoginController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java index 428a17d..f8a21ab 100644 --- a/src/main/java/com/stempo/api/domain/presentation/LoginController.java +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -24,10 +24,13 @@ public class LoginController { @PostMapping("/api/vi/login") public ApiResponse login( @RequestHeader(value = "Device-Tag", defaultValue = "490154203237518") String deviceTag, - @RequestHeader(value = "Password", required = false) String password + @RequestHeader(value = "Password", required = false) String password, + HttpServletResponse response ) { TokenInfo token = loginService.loginOrRegister(deviceTag); - return ApiResponse.success(token); + response.setHeader("Authorization", "Bearer " + token.getAccessToken()); + response.setHeader("Refresh-Token", token.getRefreshToken()); + return ApiResponse.success(); } @Operation(summary = "[U] 토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") @@ -37,7 +40,7 @@ public ApiResponse reissueToken( HttpServletResponse response ) { TokenInfo token = loginService.reissueToken(request); - response.setHeader("Authorization", "Bearer" + token.getAccessToken()); + response.setHeader("Authorization", "Bearer " + token.getAccessToken()); response.setHeader("Refresh-Token", token.getRefreshToken()); return ApiResponse.success(); } From 0b91dd24993c933d88fcea5787faaa2761a0a0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 16:56:22 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor(User):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A5=BC=20=EC=A7=80=EC=A0=95=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stempo/api/domain/application/service/LoginService.java | 2 +- .../api/domain/application/service/LoginServiceImpl.java | 4 ++-- .../stempo/api/domain/application/service/UserService.java | 2 +- .../api/domain/application/service/UserServiceImpl.java | 4 ++-- .../com/stempo/api/domain/presentation/LoginController.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginService.java b/src/main/java/com/stempo/api/domain/application/service/LoginService.java index b5e7b28..d4a4b51 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginService.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginService.java @@ -5,7 +5,7 @@ public interface LoginService { - TokenInfo loginOrRegister(String deviceTag); + TokenInfo loginOrRegister(String deviceTag, String password); TokenInfo reissueToken(HttpServletRequest request); } diff --git a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java index 525a5a5..e7410b4 100644 --- a/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/LoginServiceImpl.java @@ -19,9 +19,9 @@ public class LoginServiceImpl implements LoginService { private final JwtTokenProvider jwtTokenProvider; @Override - public TokenInfo loginOrRegister(String deviceTag) { + public TokenInfo loginOrRegister(String deviceTag, String password) { User user = userService.findById(deviceTag) - .orElseGet(() -> userService.registerUser(deviceTag)); + .orElseGet(() -> userService.registerUser(deviceTag, password)); return generateAndSaveToken(user); } diff --git a/src/main/java/com/stempo/api/domain/application/service/UserService.java b/src/main/java/com/stempo/api/domain/application/service/UserService.java index 9687e3a..2db2e85 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserService.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserService.java @@ -6,7 +6,7 @@ public interface UserService { - User registerUser(String deviceTag); + User registerUser(String deviceTag, String password); Optional findById(String id); diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index 5b7483b..b046427 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -16,8 +16,8 @@ public class UserServiceImpl implements UserService { private final PasswordService passwordService; @Override - public User registerUser(String deviceTag) { - String rawPassword = UUID.randomUUID().toString(); + public User registerUser(String deviceTag, String password) { + String rawPassword = password != null ? password : UUID.randomUUID().toString(); User user = User.create(deviceTag, rawPassword); String encodedPassword = passwordService.encodePassword(user.getPassword()); user.updatePassword(encodedPassword); diff --git a/src/main/java/com/stempo/api/domain/presentation/LoginController.java b/src/main/java/com/stempo/api/domain/presentation/LoginController.java index f8a21ab..7a6070b 100644 --- a/src/main/java/com/stempo/api/domain/presentation/LoginController.java +++ b/src/main/java/com/stempo/api/domain/presentation/LoginController.java @@ -27,7 +27,7 @@ public ApiResponse login( @RequestHeader(value = "Password", required = false) String password, HttpServletResponse response ) { - TokenInfo token = loginService.loginOrRegister(deviceTag); + TokenInfo token = loginService.loginOrRegister(deviceTag, password); response.setHeader("Authorization", "Bearer " + token.getAccessToken()); response.setHeader("Refresh-Token", token.getRefreshToken()); return ApiResponse.success(); From d2bdefa5d9bd1ebb11796a6542cd381e8bdbe57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 17:15:09 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor(User):=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/UserServiceImpl.java | 5 +- .../stempo/api/global/util/PasswordUtil.java | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/stempo/api/global/util/PasswordUtil.java diff --git a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java index b046427..7573549 100644 --- a/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java +++ b/src/main/java/com/stempo/api/domain/application/service/UserServiceImpl.java @@ -2,11 +2,11 @@ import com.stempo.api.domain.domain.model.User; import com.stempo.api.domain.domain.repository.UserRepository; +import com.stempo.api.global.util.PasswordUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.Optional; -import java.util.UUID; @Service @RequiredArgsConstructor @@ -14,10 +14,11 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final PasswordService passwordService; + private final PasswordUtil passwordUtil; @Override public User registerUser(String deviceTag, String password) { - String rawPassword = password != null ? password : UUID.randomUUID().toString(); + String rawPassword = password != null ? password : passwordUtil.generateStrongPassword(); User user = User.create(deviceTag, rawPassword); String encodedPassword = passwordService.encodePassword(user.getPassword()); user.updatePassword(encodedPassword); diff --git a/src/main/java/com/stempo/api/global/util/PasswordUtil.java b/src/main/java/com/stempo/api/global/util/PasswordUtil.java new file mode 100644 index 0000000..d2c8676 --- /dev/null +++ b/src/main/java/com/stempo/api/global/util/PasswordUtil.java @@ -0,0 +1,46 @@ +package com.stempo.api.global.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class PasswordUtil { + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + private static final String DIGITS = "0123456789"; + private static final String SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?"; + private static final String ALL = UPPER + LOWER + DIGITS + SPECIAL; + private static final SecureRandom RANDOM = new SecureRandom(); + + @Value("${security.account.password-length}") + private int passwordLength; + + public String generateStrongPassword() { + List passwordChars = new ArrayList<>(); + + // 적어도 하나의 대문자, 소문자, 숫자, 특수문자를 포함 + passwordChars.add(UPPER.charAt(RANDOM.nextInt(UPPER.length()))); + passwordChars.add(LOWER.charAt(RANDOM.nextInt(LOWER.length()))); + passwordChars.add(DIGITS.charAt(RANDOM.nextInt(DIGITS.length()))); + passwordChars.add(SPECIAL.charAt(RANDOM.nextInt(SPECIAL.length()))); + + // 나머지 길이만큼 임의의 문자로 채움 + for (int i = 4; i < passwordLength; i++) { + passwordChars.add(ALL.charAt(RANDOM.nextInt(ALL.length()))); + } + + Collections.shuffle(passwordChars, RANDOM); + + StringBuilder password = new StringBuilder(passwordLength); + for (char c : passwordChars) { + password.append(c); + } + + return password.toString(); + } +} From 021e290bf954fc56412acb196387e118acf11536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Sun, 21 Jul 2024 17:25:31 +0900 Subject: [PATCH 13/13] =?UTF-8?q?refactor(Test):=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stempo/api/ApiApplicationTests.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/java/com/stempo/api/ApiApplicationTests.java diff --git a/src/test/java/com/stempo/api/ApiApplicationTests.java b/src/test/java/com/stempo/api/ApiApplicationTests.java deleted file mode 100644 index 21ebae2..0000000 --- a/src/test/java/com/stempo/api/ApiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.stempo.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApiApplicationTests { - - @Test - void contextLoads() { - } - -}