diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 0df152e..cc3d9bf 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -6,8 +6,18 @@
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index b7d5a1d..40c5402 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,7 +7,7 @@
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 03631da..d92bb0f 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,6 +3,7 @@
+
diff --git a/.idea/modules/1984157471/greatjourney.main.iml b/.idea/modules/1984157471/greatjourney.main.iml
new file mode 100644
index 0000000..f4cd67e
--- /dev/null
+++ b/.idea/modules/1984157471/greatjourney.main.iml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/backend.greatjourney.main.iml b/.idea/modules/backend.greatjourney.main.iml
new file mode 100644
index 0000000..39e98ac
--- /dev/null
+++ b/.idea/modules/backend.greatjourney.main.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/greatjourney/.gitignore b/greatjourney/.gitignore
index cbebd39..3fa5412 100644
--- a/greatjourney/.gitignore
+++ b/greatjourney/.gitignore
@@ -29,6 +29,7 @@ out/
application.properties
+application.yml
src/main/resources/application.properties
diff --git a/greatjourney/build.gradle b/greatjourney/build.gradle
index c9b1ca5..3e34ecf 100644
--- a/greatjourney/build.gradle
+++ b/greatjourney/build.gradle
@@ -18,6 +18,7 @@ repositories {
}
dependencies {
+ implementation 'org.springframework:spring-webmvc:6.1.11'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.mysql:mysql-connector-j'
@@ -31,10 +32,10 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
//jwt
- implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
-
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// AWS SDK for S3 (v1)
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.565'
@@ -52,7 +53,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
//swagger 관련 코드
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
//jsoup 관련 코드
implementation 'org.jsoup:jsoup:1.18.1'
@@ -68,6 +69,12 @@ dependencies {
implementation 'org.apache.commons:commons-csv:1.10.0'
implementation 'org.apache.poi:poi-ooxml:5.2.5'
+
+ //querydsl
+ implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+ annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
+ annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+ annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
diff --git a/greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java b/greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java
index 14607a7..9fef16b 100644
--- a/greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java
+++ b/greatjourney/src/main/java/backend/greatjourney/GreatjourneyApplication.java
@@ -5,8 +5,8 @@
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
-@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@EnableJpaAuditing
+@SpringBootApplication
public class GreatjourneyApplication {
public static void main(String[] args) {
diff --git a/greatjourney/src/main/java/backend/greatjourney/config/QueryDslConfig.java b/greatjourney/src/main/java/backend/greatjourney/config/QueryDslConfig.java
new file mode 100644
index 0000000..70e31c3
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/config/QueryDslConfig.java
@@ -0,0 +1,21 @@
+package backend.greatjourney.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+@Configuration
+public class QueryDslConfig {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory() {
+ return new JPAQueryFactory(entityManager);
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java b/greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java
index 4e91ee3..dfd84e9 100644
--- a/greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java
+++ b/greatjourney/src/main/java/backend/greatjourney/config/SwaggerConfig.java
@@ -1,24 +1,40 @@
package backend.greatjourney.config;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
-import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
-import org.springframework.context.annotation.Bean;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+@Configuration
public class SwaggerConfig {
-// @Bean
-// public OpenAPI api() {
-// SecurityScheme apiKey = new SecurityScheme()
-// .type(SecurityScheme.Type.APIKEY)
-// .in(SecurityScheme.In.HEADER)
-// .name("Authorization");
-//
-// SecurityRequirement securityRequirement = new SecurityRequirement()
-// .addList("Bearer Token");
-//
-// return new OpenAPI()
-// .components(new Components().addSecuritySchemes("Bearer Token", apiKey))
-// .addSecurityItem(securityRequirement);
-// }
-}
+ @Bean
+ public OpenAPI openAPI() {
+ SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
+
+ return new OpenAPI()
+ .components(new Components())
+ .info(apiInfo())
+ .addSecurityItem(securityRequirement)
+ .schemaRequirement("BearerAuth", securityScheme());
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("어디로 API")
+ .description("대장정이팀 API 명세서입니다")
+ .version("1.0.0");
+ }
+
+ private SecurityScheme securityScheme() {
+ return new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .in(SecurityScheme.In.HEADER)
+ .name("Authorization");
+ }
+}
\ No newline at end of file
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java
new file mode 100644
index 0000000..6307dbd
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/dto/TokenResponse.java
@@ -0,0 +1,11 @@
+package backend.greatjourney.domain.token.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class TokenResponse {
+ private String accessToken;
+ private String refreshToken;
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java
new file mode 100644
index 0000000..9c64c86
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/entity/RefreshToken.java
@@ -0,0 +1,31 @@
+package backend.greatjourney.domain.token.entity;
+
+import java.time.Instant;
+
+import org.springframework.web.bind.annotation.RequestParam;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor
+@Getter
+public class RefreshToken {
+
+ @Id
+ private String tokenId;
+ private Long userId;
+ private String token;
+ private Instant expiryDate;
+
+ @Builder
+ private RefreshToken(Long userId, String id, String token, Instant expiryDate) {
+ this.userId = userId;
+ this.tokenId = id;
+ this.token = token;
+ this.expiryDate = expiryDate;
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/repository/RefreshTokenRepository.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/repository/RefreshTokenRepository.java
new file mode 100644
index 0000000..7f6ca68
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/repository/RefreshTokenRepository.java
@@ -0,0 +1,11 @@
+package backend.greatjourney.domain.token.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import backend.greatjourney.domain.token.entity.RefreshToken;
+
+public interface RefreshTokenRepository extends JpaRepository {
+
+ Void deleteByToken(String token);
+ RefreshToken findByUserId(Long userId);
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationFilter.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..56e8d39
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationFilter.java
@@ -0,0 +1,43 @@
+package backend.greatjourney.domain.token.service;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtTokenProvider jwtTokenProvider;
+
+ public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
+ this.jwtTokenProvider = jwtTokenProvider;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain)
+ throws ServletException, IOException {
+
+ String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String token = authHeader.substring("Bearer ".length());
+
+ if (jwtTokenProvider.validateToken(token)) {
+ UsernamePasswordAuthenticationToken authentication =
+ (UsernamePasswordAuthenticationToken) jwtTokenProvider.getAuthentication(token);
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationManager.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationManager.java
new file mode 100644
index 0000000..541cbf5
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtAuthenticationManager.java
@@ -0,0 +1,23 @@
+package backend.greatjourney.domain.token.service;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JwtAuthenticationManager implements AuthenticationManager {
+ private final JwtTokenProvider jwtTokenProvider;
+
+
+ public JwtAuthenticationManager(JwtTokenProvider jwtTokenProvider) {
+ this.jwtTokenProvider = jwtTokenProvider;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) {
+ String token = authentication.getCredentials().toString();
+ return jwtTokenProvider.getAuthentication(token);
+ }
+}
+
+
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java
new file mode 100644
index 0000000..8ca419a
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/token/service/JwtTokenProvider.java
@@ -0,0 +1,165 @@
+package backend.greatjourney.domain.token.service;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+
+import javax.crypto.SecretKey;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import backend.greatjourney.domain.token.dto.TokenResponse;
+import backend.greatjourney.domain.token.entity.RefreshToken;
+
+import backend.greatjourney.domain.token.repository.RefreshTokenRepository;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.domain.user.repository.UserRepository;
+import backend.greatjourney.global.exception.CustomException;
+import backend.greatjourney.global.exception.ErrorCode;
+import backend.greatjourney.global.security.entitiy.CustomOAuth2User;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import io.jsonwebtoken.security.MacAlgorithm;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtTokenProvider {
+
+ private final UserRepository userRepository;
+ private final RefreshTokenRepository refreshTokenRepository;
+
+ @Value("${spring.jwt.secret}")
+ private String secret;
+
+ @Value("${spring.jwt.access-token-duration}")
+ private Duration accessTokenDuration;
+
+ @Value("${spring.jwt.refresh-token-duration}")
+ private Duration refreshTokenDuration;
+
+ private final MacAlgorithm alg = Jwts.SIG.HS512;
+ private SecretKey key;
+
+ @PostConstruct
+ public void init() {
+ try {
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.JWT_KEY_GENERATION_FAILED);
+ }
+ }
+
+ public String createAccessToken(Long userId) {
+ try {
+ Date now = new Date();
+ Date expiry = new Date(now.getTime() + accessTokenDuration.toMillis());
+
+ return Jwts.builder()
+ .claims(Map.of("sub", userId))
+ .issuedAt(now)
+ .expiration(expiry)
+ .signWith(key, alg)
+ .compact();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+ public String createRefreshToken(Long userId) {
+ try {
+ Date now = new Date();
+ Date expiry = new Date(now.getTime() + refreshTokenDuration.toMillis());
+
+ return Jwts.builder()
+ .claims(Map.of("sub", userId))
+ .issuedAt(now)
+ .expiration(expiry)
+ .signWith(key, alg)
+ .compact();
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.EXPIRED_REFRESH_TOKEN);
+ }
+ }
+
+
+ public TokenResponse createToken(Long userId) {
+ try {
+ String accessToken = createAccessToken(userId);
+ String refreshToken = createRefreshToken(userId);
+
+ Instant expiryDate = Instant.now().plus(refreshTokenDuration);
+
+ RefreshToken refreshTokenEntity = RefreshToken.builder()
+ .userId(userId)
+ .token(refreshToken)
+ .expiryDate(expiryDate)
+ .build();
+
+ refreshTokenRepository.save(refreshTokenEntity);
+ return new TokenResponse(accessToken,refreshToken);
+
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+
+ public String getUserIdFromToken(String token) {
+ try {
+ return Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload()
+ .getSubject();
+ } catch (JwtException e) {
+ throw new CustomException(ErrorCode.JWT_PARSE_FAILED);
+ }
+ }
+
+ public Authentication getAuthentication(String token) {
+ try {
+ Claims claims = Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+
+ String userId = claims.getSubject();
+ Long realUserId = Long.parseLong(userId);
+ User user = userRepository.findById(realUserId)
+ .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
+
+ Map attributes = Map.of("userId", userId);
+ CustomOAuth2User principal = new CustomOAuth2User(attributes, userId, user.getUserRole().name());
+
+ return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities());
+
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.LOGIN_FAIL);
+ }
+ }
+
+ public boolean validateToken(String token) {
+ try {
+ Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java
new file mode 100644
index 0000000..bee8c9e
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/controller/UserController.java
@@ -0,0 +1,76 @@
+package backend.greatjourney.domain.user.controller;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+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;
+
+import backend.greatjourney.domain.user.dto.request.ChangeUserRequest;
+import backend.greatjourney.domain.user.dto.request.KakaoLoginRequest;
+import backend.greatjourney.domain.user.dto.request.SignUpRequest;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.domain.user.service.KakaoService;
+import backend.greatjourney.domain.user.service.UserService;
+import backend.greatjourney.global.exception.BaseResponse;
+import backend.greatjourney.global.security.entitiy.CustomOAuth2User;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping("/api/v1/user")
+@RequiredArgsConstructor
+@Tag(name = "로그인 API", description = "로그인/로그아웃 API에 대한 설명입니다.")
+public class UserController {
+
+ private final UserService userService;
+ private final KakaoService kakaoService;
+
+ //회원가입
+ @Operation(summary = "회원가입 API")
+ @PostMapping("/signup")
+ public BaseResponse signUp(@RequestBody SignUpRequest request){
+ return userService.signupUser(request);
+ }
+
+ //회원탈퇴
+ @Operation(summary = "회원탈퇴 API")
+ @DeleteMapping("/signout")
+ public BaseResponse singOut(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){
+ return userService.signOutUser(customOAuth2User);
+ }
+
+ //카카오로그인
+ @Operation(summary = "카카오로그인 API")
+ @PostMapping("/kakao")
+ public BaseResponse> loginKakao(@RequestBody KakaoLoginRequest request){
+ return BaseResponse.builder()
+ .isSuccess(true)
+ .code(200)
+ .message("로그인이 완료되었습니다.")
+ .data(kakaoService.loginWithKakao(request.accessToken()))
+ .build();
+ }
+
+ //로그아웃
+ @Operation(summary = "로그아웃 API")
+ @PostMapping("/logout")
+ public BaseResponse logout(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){
+ return userService.logOutUser(customOAuth2User);
+ }
+
+
+ //회원정보수정
+ @Operation(summary = "회원정보 수정 API")
+ @PatchMapping("/change")
+ public BaseResponse> chageUserInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User,@RequestBody
+ ChangeUserRequest request){
+ return userService.changeUserInfo(customOAuth2User,request);
+ }
+
+
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/properties/KakaoOAuthProperties.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/properties/KakaoOAuthProperties.java
new file mode 100644
index 0000000..f492ca7
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/properties/KakaoOAuthProperties.java
@@ -0,0 +1,16 @@
+package backend.greatjourney.domain.user.dto.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Configuration
+@ConfigurationProperties(prefix = "kakao")
+@Getter
+@Setter
+public class KakaoOAuthProperties {
+ private String clientId;
+ private String clientSecret;
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/ChangeUserRequest.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/ChangeUserRequest.java
new file mode 100644
index 0000000..91fa5d8
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/ChangeUserRequest.java
@@ -0,0 +1,4 @@
+package backend.greatjourney.domain.user.dto.request;
+
+public record ChangeUserRequest(String name) {
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/KakaoLoginRequest.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/KakaoLoginRequest.java
new file mode 100644
index 0000000..49dfca4
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/KakaoLoginRequest.java
@@ -0,0 +1,4 @@
+package backend.greatjourney.domain.user.dto.request;
+
+public record KakaoLoginRequest(String accessToken) {
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/SignUpRequest.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/SignUpRequest.java
new file mode 100644
index 0000000..010df1c
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/request/SignUpRequest.java
@@ -0,0 +1,5 @@
+package backend.greatjourney.domain.user.dto.request;
+
+public record SignUpRequest(
+ String email, String domain) {
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java
new file mode 100644
index 0000000..20b2111
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/dto/response/KakaoUserResponse.java
@@ -0,0 +1,29 @@
+package backend.greatjourney.domain.user.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@Getter
+@Setter
+public class KakaoUserResponse {
+ private Long id;
+ private KakaoAccount kakao_account;
+ private Properties properties;
+
+ @Getter
+ @Setter
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class KakaoAccount {
+ private String email;
+ }
+
+ @Getter
+ @Setter
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class Properties {
+ private String nickname;
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Domain.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Domain.java
new file mode 100644
index 0000000..6af4607
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Domain.java
@@ -0,0 +1,7 @@
+package backend.greatjourney.domain.user.entity;
+
+public enum Domain {
+ KAKAO,
+ GOOGLE,
+ NAVER
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Status.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Status.java
new file mode 100644
index 0000000..95c931e
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/Status.java
@@ -0,0 +1,7 @@
+package backend.greatjourney.domain.user.entity;
+
+public enum Status {
+ PENDING,
+ SUCCESS,
+ DELETED
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/User.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/User.java
new file mode 100644
index 0000000..512068a
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/User.java
@@ -0,0 +1,64 @@
+package backend.greatjourney.domain.user.entity;
+
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Table(name = "User")
+@NoArgsConstructor
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long userId;
+
+ @Setter
+ private String name;
+
+ @Setter
+ private Status status;
+
+ private Domain domain;
+
+ private String email;
+
+ private UserRole userRole;
+
+ private Terms terms;
+
+
+
+ @Builder
+ private User(Long id, Status status, Domain domain,String email,UserRole userRole, Terms terms,String name){
+ this.userId = id;
+ this.status = status;
+ this.domain = domain;
+ this.name = name;
+ this.email = email;
+ this.userRole = userRole;
+ this.terms = terms;
+ }
+
+
+ @Embeddable
+ @Getter
+ @NoArgsConstructor
+ public static class Terms {
+ private boolean marketing;
+
+ @Builder
+ private Terms(boolean marketing) {
+ this.marketing = marketing;
+ }
+ }
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/UserRole.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/UserRole.java
new file mode 100644
index 0000000..b4df7c4
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/entity/UserRole.java
@@ -0,0 +1,6 @@
+package backend.greatjourney.domain.user.entity;
+
+public enum UserRole {
+ ROLE_USER,
+ ROLE_ADMIN
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepository.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepository.java
new file mode 100644
index 0000000..7c6f3f0
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepository.java
@@ -0,0 +1,8 @@
+package backend.greatjourney.domain.user.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import backend.greatjourney.domain.user.entity.User;
+
+public interface UserRepository extends JpaRepository , UserRepositoryCustom {
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepositoryCustom.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepositoryCustom.java
new file mode 100644
index 0000000..854959c
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/UserRepositoryCustom.java
@@ -0,0 +1,11 @@
+package backend.greatjourney.domain.user.repository;
+
+import java.util.Optional;
+
+import backend.greatjourney.domain.user.entity.User;
+
+public interface UserRepositoryCustom {
+
+ boolean existsByEmail(String email);
+ Optional findByUserId(Long userId);
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java
new file mode 100644
index 0000000..3f940a1
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/repository/impl/UserRepositoryImplement.java
@@ -0,0 +1,40 @@
+package backend.greatjourney.domain.user.repository.impl;
+
+import java.util.Optional;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import backend.greatjourney.domain.user.entity.QUser;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.domain.user.repository.UserRepositoryCustom;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class UserRepositoryImplement implements UserRepositoryCustom {
+ private final JPAQueryFactory queryFactory;
+ QUser quesr = QUser.user;
+
+ @Override
+ public boolean existsByEmail(String email){
+ return queryFactory
+ .selectOne()
+ .from(quesr)
+ .where(
+ quesr.email.eq(email)
+ )
+ .fetchFirst() != null;
+ }
+
+ @Override
+ public Optional findByUserId(Long userId) {
+ return Optional.ofNullable(
+ queryFactory
+ .selectFrom(quesr)
+ .where(quesr.userId.eq(userId))
+ .fetchOne()
+ );
+ }
+
+
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java
new file mode 100644
index 0000000..87db6c6
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/KakaoService.java
@@ -0,0 +1,68 @@
+package backend.greatjourney.domain.user.service;
+
+import java.util.Map;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import aj.org.objectweb.asm.TypeReference;
+import backend.greatjourney.domain.token.dto.TokenResponse;
+import backend.greatjourney.domain.token.service.JwtTokenProvider;
+import backend.greatjourney.domain.user.dto.properties.KakaoOAuthProperties;
+import backend.greatjourney.domain.user.dto.response.KakaoUserResponse;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.global.exception.BaseException;
+import backend.greatjourney.global.exception.CustomException;
+import backend.greatjourney.global.exception.ErrorCode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KakaoService {
+
+ private final RestTemplate restTemplate = new RestTemplate();
+ private final KakaoOAuthProperties kakaoProps;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final SignService signService;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final String domain = "kakao";
+
+ public TokenResponse loginWithKakao(String accessToken) {
+ KakaoUserResponse userInfo = getUserInfo(accessToken);
+ User user = signService.saveUserKakao(userInfo, domain);
+ return jwtTokenProvider.createToken(user.getUserId());
+ }
+
+
+ private KakaoUserResponse getUserInfo(String accessToken) {
+ String url = "https://kapi.kakao.com/v2/user/me";
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(accessToken);
+ HttpEntity request = new HttpEntity<>(headers);
+
+ try {
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, request, String.class);
+ if (response.getStatusCode().is2xxSuccessful()) {
+ return objectMapper.readValue(response.getBody(), KakaoUserResponse.class);
+ } else {
+ throw new CustomException(ErrorCode.USER_NOT_FOUND);
+ }
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.KAKAO_USER_ERROR);
+ }
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java
new file mode 100644
index 0000000..3beff9c
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/SignService.java
@@ -0,0 +1,39 @@
+package backend.greatjourney.domain.user.service;
+
+import org.apache.xmlbeans.impl.store.DomImpl;
+import org.springframework.stereotype.Service;
+
+import backend.greatjourney.domain.user.dto.response.KakaoUserResponse;
+import backend.greatjourney.domain.user.entity.Domain;
+import backend.greatjourney.domain.user.entity.Status;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.domain.user.entity.UserRole;
+import backend.greatjourney.domain.user.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class SignService {
+
+ private final UserRepository userRepository;
+
+ public User saveUserKakao(KakaoUserResponse userInfo, String domain){
+
+ Domain realDomain = Domain.valueOf(domain);
+
+ User user = User.builder()
+ .userRole(UserRole.ROLE_USER)
+ .domain(realDomain)
+ .email(userInfo.getKakao_account().getEmail())
+ .name(userInfo.getProperties().getNickname())
+ .status(Status.PENDING)
+ .build();
+
+ return userRepository.save(user);
+
+ }
+
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java
new file mode 100644
index 0000000..b5459c1
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/domain/user/service/UserService.java
@@ -0,0 +1,93 @@
+package backend.greatjourney.domain.user.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import backend.greatjourney.domain.token.entity.RefreshToken;
+import backend.greatjourney.domain.token.repository.RefreshTokenRepository;
+import backend.greatjourney.domain.token.service.JwtTokenProvider;
+import backend.greatjourney.domain.user.dto.request.ChangeUserRequest;
+import backend.greatjourney.domain.user.dto.request.SignUpRequest;
+import backend.greatjourney.domain.user.entity.Domain;
+import backend.greatjourney.domain.user.entity.Status;
+import backend.greatjourney.domain.user.entity.User;
+import backend.greatjourney.domain.user.repository.UserRepository;
+import backend.greatjourney.global.exception.BaseResponse;
+import backend.greatjourney.global.exception.CustomException;
+import backend.greatjourney.global.exception.ErrorCode;
+import backend.greatjourney.global.security.entitiy.CustomOAuth2User;
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class UserService {
+
+ private final UserRepository userRepository;
+ private final RefreshTokenRepository refreshTokenRepository;
+ private final JwtTokenProvider tokenProvider;
+
+ @Transactional
+ public BaseResponse signupUser(SignUpRequest request){
+ if(userRepository.existsByEmail(request.email())){
+ throw new IllegalArgumentException("이미 존재하는 회원입니다.");
+ }
+ return new BaseResponse<>(true,"회원가입이 완료되었습니다",201,userRepository.save(User.builder()
+ .domain(Domain.valueOf(request.domain()))
+ .email(request.email())
+ .status(Status.SUCCESS)
+ .build()));
+ }
+
+ @Transactional
+ public BaseResponse logOutUser(CustomOAuth2User customOAuth2User){
+
+ Long userId = Long.parseLong(customOAuth2User.getUserId());
+ RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId);
+
+ return BaseResponse.builder()
+ .code(200)
+ .message("로그아웃이 완료되었습니다.")
+ .data(refreshTokenRepository.deleteByToken(refreshToken.getToken()))
+ .isSuccess(true)
+ .build();
+ }
+
+
+ @Transactional
+ public BaseResponse signOutUser(CustomOAuth2User customOAuth2User){
+ Long userId = Long.parseLong(customOAuth2User.getUserId());
+ RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId);
+
+ refreshTokenRepository.deleteByToken(refreshToken.getToken());
+ User user = userRepository.findByUserId(userId)
+ .orElseThrow(()->new CustomException(ErrorCode.USER_NOT_FOUND));
+
+ user.setStatus(Status.DELETED);
+ userRepository.save(user);
+
+ return BaseResponse.builder()
+ .isSuccess(true)
+ .code(200)
+ .message("회원탈퇴가 완료되었습니다.")
+ .data(null)
+ .build();
+ }
+
+ @Transactional
+ public BaseResponse changeUserInfo(CustomOAuth2User customOAuth2User, ChangeUserRequest request){
+ Long userId = Long.parseLong(customOAuth2User.getUserId());
+ User user = userRepository.findById(userId)
+ .orElseThrow(()->new CustomException(ErrorCode.USER_NOT_FOUND));
+
+ user.setName(request.name());
+ userRepository.save(user);
+ return BaseResponse.builder()
+ .isSuccess(true)
+ .code(200)
+ .message("회원정보 수정이 완료되었습니다.")
+ .data(null)
+ .build();
+ }
+
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java b/greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java
index eb9e2c7..9270bff 100644
--- a/greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java
+++ b/greatjourney/src/main/java/backend/greatjourney/global/exception/BaseResponse.java
@@ -20,7 +20,6 @@ public BaseResponse(boolean isSuccess, String message, int code, String data) {
this.code = code;
this.data = (T) data;
}
-
@Builder
public BaseResponse(boolean isSuccess, String message, int code, T data) {
this.isSuccess = isSuccess;
diff --git a/greatjourney/src/main/java/backend/greatjourney/global/exception/CustomException.java b/greatjourney/src/main/java/backend/greatjourney/global/exception/CustomException.java
new file mode 100644
index 0000000..6a28549
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/global/exception/CustomException.java
@@ -0,0 +1,14 @@
+package backend.greatjourney.global.exception;
+
+public class CustomException extends RuntimeException{
+ private final ErrorCode errorCode;
+
+ public CustomException(ErrorCode errorCode) {
+ super(errorCode.getMessage());
+ this.errorCode = errorCode;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java b/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java
new file mode 100644
index 0000000..c9ae0bc
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java
@@ -0,0 +1,38 @@
+package backend.greatjourney.global.exception;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+
+@Getter
+public enum ErrorCode {
+ LOGIN_FAIL(HttpStatus.BAD_REQUEST,400,"로그인에 오류가 발생하였습니다."),
+ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500,"서버에 오류가 발생하였습니다."),
+ JWT_KEY_GENERATION_FAILED(HttpStatus.BAD_REQUEST,400,"JWT 키 생성에 실패하였습니다."),
+ NO_REFRESH_TOKEN(UNAUTHORIZED,400, "리프레시 토큰이 없습니다."),
+ LOGOUT_ERROR(BAD_REQUEST,400,"로그아웃에 실패하였습니다."),
+ SIGNUP_ERROR(BAD_REQUEST,400,"회원가입에러입니다."),
+ EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, 400,"만료된 토큰입니다."),
+ JWT_PARSE_FAILED(BAD_REQUEST,404,"토큰 파싱이 잘못되었습니다."),
+
+ KAKAO_USER_ERROR(BAD_REQUEST,404,"카카오 유저 정보를 가져오지 못하였습니다."),
+ TOKEN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,500, "토큰을 제대로 생성하지 못하였습니다."),
+
+
+ USER_NOT_FOUND(BAD_REQUEST,404,"존재하지 않는 유저입니다.");
+
+
+
+ private final HttpStatus status;
+ private final int code;
+ private final String message;
+
+
+ ErrorCode( HttpStatus status,int code, String message){
+ this.code = code;
+ this.status = status;
+ this.message = message;
+ }
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/global/security/config/SecurityConfig.java b/greatjourney/src/main/java/backend/greatjourney/global/security/config/SecurityConfig.java
new file mode 100644
index 0000000..8d8dae7
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/global/security/config/SecurityConfig.java
@@ -0,0 +1,52 @@
+package backend.greatjourney.global.security.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+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;
+
+import backend.greatjourney.domain.token.service.JwtAuthenticationFilter;
+import backend.greatjourney.domain.token.service.JwtTokenProvider;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+
+@Configuration
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .httpBasic(AbstractHttpConfigurer::disable)
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(session ->
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/auth/**","api/v1/","/v3/api-docs/**",
+ "/swagger-ui/**", // ✅ Swagger UI HTML/JS
+ "/swagger-ui.html", // ✅ Swagger 메인 HTML
+ "/webjars/**" ) // ✅ Swagger 리소스
+ .permitAll()
+ .anyRequest().authenticated()
+ )
+ .exceptionHandling(e -> e
+ .authenticationEntryPoint((request, response, authException) -> {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+ })
+ )
+ .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
+ UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+
+}
diff --git a/greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java b/greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java
new file mode 100644
index 0000000..1b56e46
--- /dev/null
+++ b/greatjourney/src/main/java/backend/greatjourney/global/security/entitiy/CustomOAuth2User.java
@@ -0,0 +1,47 @@
+package backend.greatjourney.global.security.entitiy;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import lombok.Getter;
+
+
+public class CustomOAuth2User implements OAuth2User {
+
+ private final Map attributes; //여기에 인증서버로 부터 받은 사용자 정보가 담김
+
+ private final String uuid;
+
+ @Getter
+ private final String role;
+
+ public CustomOAuth2User(Map attributes, String uuid, String role) {
+ this.attributes = attributes;
+ this.uuid = uuid;
+ this.role = role;
+ }
+
+ public String getUserId() {
+ return uuid;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public String getName() {
+ return uuid;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(() -> role);
+ }
+
+}
\ No newline at end of file