diff --git a/service/auth/server/src/main/java/com/sparta/auth/server/application/service/AuthService.java b/service/auth/server/src/main/java/com/sparta/auth/server/application/service/AuthService.java index c596d6c5..7d734945 100644 --- a/service/auth/server/src/main/java/com/sparta/auth/server/application/service/AuthService.java +++ b/service/auth/server/src/main/java/com/sparta/auth/server/application/service/AuthService.java @@ -21,6 +21,7 @@ import java.util.Map; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @@ -29,18 +30,22 @@ public class AuthService { private final UserService userService; private final JwtProperties jwtProperties; private final SecretKey secretKey; + private final PasswordEncoder passwordEncoder; - public AuthService(UserService userService, JwtProperties jwtProperties) { + public AuthService(UserService userService, JwtProperties jwtProperties, + PasswordEncoder passwordEncoder) { this.userService = userService; this.jwtProperties = jwtProperties; this.secretKey = createSecretKey(); + this.passwordEncoder = passwordEncoder; } public AuthResponse.SignIn signIn(AuthRequest.SignIn request) { UserDto userData = userService.getUserByUsername(request.getUsername()); - // TODO(경민): 암호화 된 비밀번호로 비교해야함 - if (userData == null || !userData.getPassword().equals(request.getPassword())) { + if (userData == null || + !passwordEncoder.matches(request.getPassword(), userData.getPassword()) + ) { throw new AuthException(AuthErrorCode.SIGN_IN_FAIL); } diff --git a/service/auth/server/src/main/java/com/sparta/auth/server/infrastructure/configuration/SecurityConfig.java b/service/auth/server/src/main/java/com/sparta/auth/server/infrastructure/configuration/SecurityConfig.java index 9d8263f0..9c7d97fc 100644 --- a/service/auth/server/src/main/java/com/sparta/auth/server/infrastructure/configuration/SecurityConfig.java +++ b/service/auth/server/src/main/java/com/sparta/auth/server/infrastructure/configuration/SecurityConfig.java @@ -6,6 +6,8 @@ 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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @@ -30,4 +32,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + } diff --git a/service/auth/server/src/test/java/com/sparta/auth/server/AuthApplicationTests.java b/service/auth/server/src/test/java/com/sparta/auth/server/AuthApplicationTests.java index 657cd44d..1a3e5e92 100644 --- a/service/auth/server/src/test/java/com/sparta/auth/server/AuthApplicationTests.java +++ b/service/auth/server/src/test/java/com/sparta/auth/server/AuthApplicationTests.java @@ -56,8 +56,9 @@ void setUp() { @Test void test_로그인_성공() { // Arrange - AuthRequest.SignIn request = new AuthRequest.SignIn("testuser", "correctpassword"); - UserDto userDto = new UserDto(1L, "testuser", "correctpassword", "ROLE_ADMIN"); + AuthRequest.SignIn request = new AuthRequest.SignIn("testuser", "password123"); + UserDto userDto = new UserDto(1L, "testuser", + "$2a$10$YZ2cP0PF11iqNqNrwk4pUOKQnAGxqLtGxO1F6XZomixg73EYQoduC", "ROLE_ADMIN"); // Mock the user service to return the userDto when called when(userService.getUserByUsername("testuser")).thenReturn(userDto); diff --git a/service/gateway/server/build.gradle b/service/gateway/server/build.gradle index 29ff340e..f99e4be8 100644 --- a/service/gateway/server/build.gradle +++ b/service/gateway/server/build.gradle @@ -1,49 +1,53 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.4' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' } group = 'com.sparta.gateway' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } ext { - set('springCloudVersion', "2023.0.3") + set('springCloudVersion', "2023.0.3") } dependencies { - implementation 'org.springframework.cloud:spring-cloud-starter-gateway' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'io.projectreactor:reactor-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation project(':common:domain') + implementation project(':service:auth:auth_dto') + + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.cloud:spring-cloud-starter-gateway' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/service/gateway/server/src/main/java/com/sparta/gateway/server/GatewayApplication.java b/service/gateway/server/src/main/java/com/sparta/gateway/server/GatewayApplication.java index bdd94417..db323fb4 100644 --- a/service/gateway/server/src/main/java/com/sparta/gateway/server/GatewayApplication.java +++ b/service/gateway/server/src/main/java/com/sparta/gateway/server/GatewayApplication.java @@ -2,12 +2,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.cloud.openfeign.EnableFeignClients; -@SpringBootApplication +@EnableFeignClients +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class GatewayApplication { - public static void main(String[] args) { - SpringApplication.run(GatewayApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } } diff --git a/service/gateway/server/src/main/java/com/sparta/gateway/server/application/AuthService.java b/service/gateway/server/src/main/java/com/sparta/gateway/server/application/AuthService.java new file mode 100644 index 00000000..579a5247 --- /dev/null +++ b/service/gateway/server/src/main/java/com/sparta/gateway/server/application/AuthService.java @@ -0,0 +1,9 @@ +package com.sparta.gateway.server.application; + +import com.sparta.auth.auth_dto.jwt.JwtClaim; + +public interface AuthService { + + JwtClaim verifyToken(String token); + +} diff --git a/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/configuration/AuthFeignConfig.java b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/configuration/AuthFeignConfig.java new file mode 100644 index 00000000..8ea37a5e --- /dev/null +++ b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/configuration/AuthFeignConfig.java @@ -0,0 +1,23 @@ +package com.sparta.gateway.server.infrastructure.configuration; + +import feign.Logger; +import feign.codec.Decoder; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.context.annotation.Bean; + +public class AuthFeignConfig { + + @Bean + public Decoder feignDecoder() { + ObjectFactory messageConverters = HttpMessageConverters::new; + return new SpringDecoder(messageConverters); + } + + @Bean + public Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } + +} diff --git a/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/feign/AuthClient.java b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/feign/AuthClient.java new file mode 100644 index 00000000..b947fde8 --- /dev/null +++ b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/feign/AuthClient.java @@ -0,0 +1,16 @@ +package com.sparta.gateway.server.infrastructure.feign; + +import com.sparta.auth.auth_dto.jwt.JwtClaim; +import com.sparta.gateway.server.application.AuthService; +import com.sparta.gateway.server.infrastructure.configuration.AuthFeignConfig; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "auth", configuration = AuthFeignConfig.class) +public interface AuthClient extends AuthService { + + @GetMapping("/internal/auth/verify") + JwtClaim verifyToken(@RequestHeader("Authorization") String token); + +} diff --git a/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/filter/JwtAuthenticationFilter.java b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..09edec8b --- /dev/null +++ b/service/gateway/server/src/main/java/com/sparta/gateway/server/infrastructure/filter/JwtAuthenticationFilter.java @@ -0,0 +1,80 @@ +package com.sparta.gateway.server.infrastructure.filter; + +import static com.sparta.common.domain.jwt.JwtGlobalConstant.AUTHORIZATION; +import static com.sparta.common.domain.jwt.JwtGlobalConstant.BEARER_PREFIX; +import static com.sparta.common.domain.jwt.JwtGlobalConstant.X_USER_CLAIMS; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sparta.auth.auth_dto.jwt.JwtClaim; +import com.sparta.gateway.server.application.AuthService; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +public class JwtAuthenticationFilter implements GlobalFilter { + + private final AuthService authService; + + public JwtAuthenticationFilter(@Lazy AuthService authService) { + this.authService = authService; + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + String path = exchange.getRequest().getURI().getPath(); + + if (path.startsWith("/api/auth/") || path.startsWith("/api/users/sign-up")) { + return chain.filter(exchange); + } + + Optional token = this.extractToken(exchange); + + if (token.isEmpty()) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); + } + + try { + JwtClaim claims = authService.verifyToken(token.get()); + this.addUserClaimsToHeaders(exchange, claims); + } catch (Exception e) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + + private void addUserClaimsToHeaders(ServerWebExchange exchange, JwtClaim claims) { + if (claims != null) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + String jsonClaims = objectMapper.writeValueAsString(claims); + exchange.getRequest().mutate() + .header(X_USER_CLAIMS, URLEncoder.encode(jsonClaims, StandardCharsets.UTF_8)) + .build(); + } catch (JsonProcessingException e) { + log.error("Error processing JSON: {}", e.getMessage()); + } + } + } + + private Optional extractToken(ServerWebExchange exchange) { + String header = exchange.getRequest().getHeaders().getFirst(AUTHORIZATION); + if (header != null && header.startsWith(BEARER_PREFIX)) { + return Optional.of(header.substring(BEARER_PREFIX.length())); + } + return Optional.empty(); + } + +} diff --git a/service/user/server/build.gradle b/service/user/server/build.gradle index 722353a1..9941c4ca 100644 --- a/service/user/server/build.gradle +++ b/service/user/server/build.gradle @@ -30,11 +30,14 @@ ext { dependencies { implementation project(':common:domain') implementation project(':service:user:user_dto') + implementation project(':service:auth:auth_dto') implementation 'mysql:mysql-connector-java:8.0.33' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' diff --git a/service/user/server/src/main/java/com/sparta/user/application/service/UserService.java b/service/user/server/src/main/java/com/sparta/user/application/service/UserService.java index 6bfb85c1..a8732e91 100644 --- a/service/user/server/src/main/java/com/sparta/user/application/service/UserService.java +++ b/service/user/server/src/main/java/com/sparta/user/application/service/UserService.java @@ -3,12 +3,13 @@ import static com.sparta.user.exception.UserErrorCode.USER_CONFLICT; import static com.sparta.user.exception.UserErrorCode.USER_NOT_FOUND; -import com.sparta.user.user_dto.infrastructure.UserDto; import com.sparta.user.domain.model.User; import com.sparta.user.domain.repository.UserRepository; import com.sparta.user.exception.UserException; import com.sparta.user.presentation.request.UserRequest; +import com.sparta.user.user_dto.infrastructure.UserDto; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +18,7 @@ public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Transactional public void createUser(UserRequest.Create request) { @@ -26,8 +28,7 @@ public void createUser(UserRequest.Create request) { user -> { throw new UserException(USER_CONFLICT); }); - // TODO: 비밀번호 암호화 필요(암호화 시 security 라이브러리가 필요한데 설치하면 테스트 불편함. 이후에 추가) - 경민 - userRepository.save(User.create(request, request.getPassword())); + userRepository.save(User.create(request, passwordEncoder.encode(request.getPassword()))); } public UserDto getUserByUsername(String username) { diff --git a/service/user/server/src/main/java/com/sparta/user/infrastructure/configuration/SecurityConfig.java b/service/user/server/src/main/java/com/sparta/user/infrastructure/configuration/SecurityConfig.java new file mode 100644 index 00000000..6addc9b8 --- /dev/null +++ b/service/user/server/src/main/java/com/sparta/user/infrastructure/configuration/SecurityConfig.java @@ -0,0 +1,52 @@ +package com.sparta.user.infrastructure.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sparta.user.infrastructure.filter.SecurityContextFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.annotation.web.configurers.RequestCacheConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableMethodSecurity +@EnableWebSecurity +@RequiredArgsConstructor +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain httpSecurity(HttpSecurity http, ObjectMapper objectMapper) + throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement((s) -> s.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) + .rememberMe(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .requestCache(RequestCacheConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/api/users/sign-up").permitAll() + .requestMatchers("/internal/users/**").permitAll() + .anyRequest().authenticated()) + .addFilterAfter(new SecurityContextFilter(objectMapper), + UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/JwtAuthentication.java b/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/JwtAuthentication.java new file mode 100644 index 00000000..160c657c --- /dev/null +++ b/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/JwtAuthentication.java @@ -0,0 +1,60 @@ +package com.sparta.user.infrastructure.filter; + +import com.sparta.auth.auth_dto.jwt.JwtClaim; +import java.util.Collection; +import java.util.Collections; +import lombok.Builder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Builder +public class JwtAuthentication implements Authentication { + + private Long userId; + private String username; + private String role; + + public static JwtAuthentication create(JwtClaim claims) { + return JwtAuthentication.builder() + .userId(claims.getUserId()) + .username(claims.getUsername()) + .role(claims.getRole()) + .build(); + } + + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority(role)); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return null; + } + + @Override + public Object getPrincipal() { + return JwtClaim.create(userId, username, role); + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + } + + @Override + public String getName() { + return username; + } + +} diff --git a/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/SecurityContextFilter.java b/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/SecurityContextFilter.java new file mode 100644 index 00000000..2f4dcc09 --- /dev/null +++ b/service/user/server/src/main/java/com/sparta/user/infrastructure/filter/SecurityContextFilter.java @@ -0,0 +1,47 @@ +package com.sparta.user.infrastructure.filter; + +import static com.sparta.common.domain.jwt.JwtGlobalConstant.X_USER_CLAIMS; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sparta.auth.auth_dto.jwt.JwtClaim; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@RequiredArgsConstructor +@Component +public class SecurityContextFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String userClaimsHeader = request.getHeader(X_USER_CLAIMS); + + if (userClaimsHeader != null) { + try { + String decodedClaims = URLDecoder.decode(userClaimsHeader, StandardCharsets.UTF_8); + JwtClaim jwtClaim = objectMapper.readValue(decodedClaims, JwtClaim.class); + SecurityContextHolder.getContext().setAuthentication(JwtAuthentication.create(jwtClaim)); + } catch (JsonProcessingException e) { + log.error("Failed to parse X-User-Claims header", e); + } + } + + filterChain.doFilter(request, response); + } + +} diff --git a/service/user/server/src/main/java/com/sparta/user/presentation/controller/UserController.java b/service/user/server/src/main/java/com/sparta/user/presentation/controller/UserController.java index 4b763d59..fc8ae262 100644 --- a/service/user/server/src/main/java/com/sparta/user/presentation/controller/UserController.java +++ b/service/user/server/src/main/java/com/sparta/user/presentation/controller/UserController.java @@ -1,9 +1,13 @@ package com.sparta.user.presentation.controller; +import com.sparta.auth.auth_dto.jwt.JwtClaim; import com.sparta.common.domain.response.ApiResponse; import com.sparta.user.application.service.UserService; import com.sparta.user.presentation.request.UserRequest; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,4 +26,15 @@ public ApiResponse createUser(@RequestBody UserRequest.Create request) { return ApiResponse.created(null); } + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/hello") + public String hello( + @AuthenticationPrincipal JwtClaim claim + ) { + System.out.println("claim.getUserId() = " + claim.getUserId()); + System.out.println("claim.getUsername() = " + claim.getUsername()); + System.out.println("claim.getRole() = " + claim.getRole()); + return "Hello World!"; + } + } diff --git a/service/user/server/src/test/java/com/sparta/user/http/UserApiTest.http b/service/user/server/src/test/java/com/sparta/user/http/UserApiTest.http index 92e87bab..db21d7aa 100644 --- a/service/user/server/src/test/java/com/sparta/user/http/UserApiTest.http +++ b/service/user/server/src/test/java/com/sparta/user/http/UserApiTest.http @@ -11,3 +11,7 @@ Content-Type: application/json "point": 0, "role": "관리자" } + +### 인증/인가 테스트 API +GET http://localhost:{{Port}}/api/users/hello +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJVU0VSX05BTUUiOiJuZXdVc2VyIiwiVVNFUl9ST0xFIjoiUk9MRV9BRE1JTiIsIlVTRVJfSUQiOjcsImlhdCI6MTcyODI3ODEzOSwiZXhwIjoxNzMxODc4MTM5fQ.V9G1wEgl9xRGgG_0wbobjVNggATGvZekewfoUMb0RNs