From 12454411fd2d34e7060081b1d1e3b23588de3588 Mon Sep 17 00:00:00 2001 From: can019 Date: Mon, 8 Sep 2025 18:58:03 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20dto=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/LoginRequestDto.java | 15 +++++ .../domain/auth/model/AuthCredential.java | 62 +++++++++++++++++++ .../auth/service/AuthCredentialAdapter.java | 26 ++++++++ 3 files changed, 103 insertions(+) create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/LoginRequestDto.java create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/LoginRequestDto.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/LoginRequestDto.java new file mode 100644 index 00000000..081d2016 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/LoginRequestDto.java @@ -0,0 +1,15 @@ +package com.gltkorea.icebang.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class LoginRequestDto { + @NotBlank(message = "이메일은 필수입니다") + @Email(message = "올바른 이메일 형식이 아닙니다") + private String email; + + @NotBlank(message = "비밃번호는 필수입니다") + private String password; +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java new file mode 100644 index 00000000..439bdf38 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java @@ -0,0 +1,62 @@ +package com.gltkorea.icebang.domain.auth.model; + +import java.math.BigInteger; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthCredential implements UserDetails { + + private BigInteger id; + private String email; + private String password; + private String status; + + // roles -> Spring Security authority로 변환 + private List roles; + + @Override + public Collection getAuthorities() { + return roles.stream() + .map(SimpleGrantedAuthority::new) // "ROLE_USER", "ROLE_ADMIN" 이런 값 + .collect(Collectors.toList()); + } + + @Override + public String getUsername() { + return email; // 로그인 ID는 email + } + + @Override + public boolean isAccountNonExpired() { + return true; // 필요 시 status 기반으로 변경 가능 + } + + @Override + public boolean isAccountNonLocked() { + return !"LOCKED".equalsIgnoreCase(status); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return !"DISABLED".equalsIgnoreCase(status); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java new file mode 100644 index 00000000..f4748166 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java @@ -0,0 +1,26 @@ +package com.gltkorea.icebang.domain.auth.service; + +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; + +import com.gltkorea.icebang.domain.auth.model.AuthCredential; +import com.gltkorea.icebang.mapper.AuthMapper; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthCredentialAdapter implements UserDetailsService { + private final AuthMapper authMapper; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + AuthCredential user = authMapper.findByEmail(email); + if (user == null) { + throw new UsernameNotFoundException("이메일 없음: " + email); + } + return user; + } +} From 61b057c5459bb08b702896c82beaaed8ec763c2e Mon Sep 17 00:00:00 2001 From: can019 Date: Tue, 9 Sep 2025 17:44:12 +0900 Subject: [PATCH 2/5] feat: login controller --- .../config/security/SecurityConfig.java | 38 +++++++++++++++++++ .../auth/controller/AuthController.java | 14 +++++++ .../auth/service/AuthCredentialAdapter.java | 11 +++--- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java index 4a2fff36..d9f7e11b 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java @@ -5,14 +5,22 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; import com.gltkorea.icebang.config.security.endpoints.SecurityEndpoints; +import com.gltkorea.icebang.domain.auth.service.AuthCredentialAdapter; import lombok.RequiredArgsConstructor; @@ -20,6 +28,21 @@ @RequiredArgsConstructor public class SecurityConfig { private final Environment environment; + private final AuthCredentialAdapter userDetailsService; + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(bCryptPasswordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } @Bean public SecureRandom secureRandom() { @@ -73,4 +96,19 @@ public PasswordEncoder bCryptPasswordEncoder() { } return new BCryptPasswordEncoder(); } + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("http://localhost:3000"); // 프론트 주소 + config.addAllowedOrigin("https://admin.icebang.site"); // 프론트 주소 + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setAllowCredentials(true); // 세션 쿠키 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java index 5da466f6..b498119e 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java @@ -1,9 +1,13 @@ package com.gltkorea.icebang.domain.auth.controller; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import com.gltkorea.icebang.common.dto.ApiResponse; +import com.gltkorea.icebang.domain.auth.dto.LoginRequestDto; import com.gltkorea.icebang.domain.auth.dto.RegisterDto; import com.gltkorea.icebang.domain.auth.service.AuthService; @@ -15,6 +19,7 @@ @RequiredArgsConstructor public class AuthController { private final AuthService authService; + private final AuthenticationManager authenticationManager; @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) @@ -22,4 +27,13 @@ public ApiResponse register(@Valid @RequestBody RegisterDto registerDto) { authService.registerUser(registerDto); return ApiResponse.success(null); } + + @PostMapping("/login") + public ApiResponse login(@RequestBody LoginRequestDto request) { + UsernamePasswordAuthenticationToken token = + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); + + Authentication auth = authenticationManager.authenticate(token); + return ApiResponse.success(auth); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java index f4748166..f3e55fa2 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java @@ -17,10 +17,11 @@ public class AuthCredentialAdapter implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - AuthCredential user = authMapper.findByEmail(email); - if (user == null) { - throw new UsernameNotFoundException("이메일 없음: " + email); - } - return user; + // AuthCredential user = authMapper.findByEmail(email); + // if (user == null) { + // throw new UsernameNotFoundException("이메일 없음: " + email); + // } + // return user; + return new AuthCredential(); } } From c508ef782f9c0e6af06016a3d89ab62b2559efb3 Mon Sep 17 00:00:00 2001 From: can019 Date: Tue, 9 Sep 2025 18:29:51 +0900 Subject: [PATCH 3/5] feat: Login api --- .../config/security/SecurityConfig.java | 2 +- .../auth/service/AuthCredentialAdapter.java | 24 ++++++++++++++----- .../gltkorea/icebang/mapper/AuthMapper.java | 3 +++ .../resources/mybatis/mapper/AuthMapper.xml | 17 +++++++++++++ .../sql/01-insert-internal-users.sql | 2 +- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java index d9f7e11b..7e32d96f 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java @@ -90,7 +90,7 @@ public PasswordEncoder bCryptPasswordEncoder() { String[] activeProfiles = environment.getActiveProfiles(); for (String profile : activeProfiles) { - if ("dev".equals(profile) || "test".equals(profile)) { + if ("develop".equals(profile) || "test".equals(profile)) { return NoOpPasswordEncoder.getInstance(); } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java index f3e55fa2..3343785c 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java @@ -1,5 +1,8 @@ package com.gltkorea.icebang.domain.auth.service; +import java.util.Arrays; +import java.util.Collections; + import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -17,11 +20,20 @@ public class AuthCredentialAdapter implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - // AuthCredential user = authMapper.findByEmail(email); - // if (user == null) { - // throw new UsernameNotFoundException("이메일 없음: " + email); - // } - // return user; - return new AuthCredential(); + AuthCredential user = authMapper.findUserByEmail(email); + + if (user == null) { + throw new UsernameNotFoundException("User not found with email: " + email); + } + + // roles가 "ROLE_USER,ROLE_ADMIN" 형태의 문자열이라면 List로 변환 + if (user.getRoles() != null && !user.getRoles().isEmpty()) { + String rolesString = user.getRoles().get(0); // GROUP_CONCAT 결과는 첫 번째 요소에 있음 + user.setRoles(Arrays.asList(rolesString.split(","))); + } else { + user.setRoles(Collections.emptyList()); + } + + return user; } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/AuthMapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/AuthMapper.java index 09033730..4480daf0 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/AuthMapper.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/AuthMapper.java @@ -3,9 +3,12 @@ import org.apache.ibatis.annotations.Mapper; import com.gltkorea.icebang.domain.auth.dto.RegisterDto; +import com.gltkorea.icebang.domain.auth.model.AuthCredential; @Mapper public interface AuthMapper { + AuthCredential findUserByEmail(String email); + boolean existsByEmail(String email); int insertUser(RegisterDto dto); // users insert diff --git a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml index c503e76e..a07b1d6f 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml @@ -12,6 +12,23 @@ ) + + + INSERT INTO user (name, email, password) VALUES (#{name}, #{email}, #{password}); diff --git a/apps/user-service/src/main/resources/sql/01-insert-internal-users.sql b/apps/user-service/src/main/resources/sql/01-insert-internal-users.sql index 3a8529c8..1a69076e 100644 --- a/apps/user-service/src/main/resources/sql/01-insert-internal-users.sql +++ b/apps/user-service/src/main/resources/sql/01-insert-internal-users.sql @@ -147,7 +147,7 @@ INSERT INTO `user` (`name`, `email`, `password`, `status`) VALUES ('홍크롤러', 'crawler.hong@icebang.site', '$2a$10$encrypted_password_hash6', 'ACTIVE'), ('서데이터', 'data.seo@icebang.site', '$2a$10$encrypted_password_hash7', 'ACTIVE'), ('윤워크플로', 'workflow.yoon@icebang.site', '$2a$10$encrypted_password_hash8', 'ACTIVE'), - ('시스템관리자', 'admin@icebang.site', '$2a$10$encrypted_password_hash0', 'ACTIVE'); + ('시스템관리자', 'admin@icebang.site', 'qwer1234!A', 'ACTIVE'); -- 8. icebang 직원-조직 연결 INSERT INTO `user_organization` (`user_id`, `organization_id`, `position_id`, `department_id`, `employee_number`, `status`) VALUES From d91bfb8cbf9630b03d957d8dd29212282e9a3485 Mon Sep 17 00:00:00 2001 From: can019 Date: Tue, 9 Sep 2025 20:05:32 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit permission 해결 안됨 --- .../typehandler/StringListTypeHandler.java | 50 +++++++++++++++++++ .../config/security/SecurityConfig.java | 43 ++++++++++------ .../security/endpoints/SecurityEndpoints.java | 13 +---- .../auth/controller/AuthController.java | 23 ++++++++- .../domain/auth/model/AuthCredential.java | 23 ++++++++- .../auth/service/AuthCredentialAdapter.java | 11 ---- .../src/main/resources/application.yml | 3 +- .../src/main/resources/log4j2-develop.yml | 7 +++ .../resources/mybatis/mapper/AuthMapper.xml | 5 +- 9 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/config/mybatis/typehandler/StringListTypeHandler.java diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/mybatis/typehandler/StringListTypeHandler.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/mybatis/typehandler/StringListTypeHandler.java new file mode 100644 index 00000000..4363124c --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/mybatis/typehandler/StringListTypeHandler.java @@ -0,0 +1,50 @@ +package com.gltkorea.icebang.config.mybatis.typehandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +@MappedTypes(List.class) +@MappedJdbcTypes(JdbcType.VARCHAR) +public class StringListTypeHandler extends BaseTypeHandler> { + + @Override + public void setNonNullParameter( + PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, String.join(",", parameter)); + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return convertToList(value); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return convertToList(value); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return convertToList(value); + } + + private List convertToList(String value) { + if (value == null || value.trim().isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(value.split(",")); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java index 7e32d96f..69ee1bf0 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java @@ -57,31 +57,46 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .permitAll() .requestMatchers("/auth/login", "/auth/logout") .permitAll() + .requestMatchers("/v0/auth/check-session") + .authenticated() .requestMatchers(SecurityEndpoints.DATA_ADMIN.getMatchers()) - .hasAuthority("SUPER_ADMIN") + .hasRole("SUPER_ADMIN") // hasAuthority -> hasRole .requestMatchers(SecurityEndpoints.DATA_ENGINEER.getMatchers()) - .hasAnyAuthority( - "SUPER_ADMIN", "ADMIN", "SENIOR_DATA_ENGINEER", "DATA_ENGINEER") + .hasAnyRole( + "SUPER_ADMIN", + "SYSTEM_ADMIN", + "AI_ENGINEER", + "DATA_SCIENTIST", + "CRAWLING_ENGINEER", + "TECH_LEAD", + "DEVOPS") .requestMatchers(SecurityEndpoints.ANALYST.getMatchers()) - .hasAnyAuthority( + .hasAnyRole( "SUPER_ADMIN", - "ADMIN", - "SENIOR_DATA_ENGINEER", - "DATA_ENGINEER", - "SENIOR_DATA_ANALYST", - "DATA_ANALYST", - "VIEWER") + "SYSTEM_ADMIN", + "ORG_ADMIN", + "DATA_SCIENTIST", + "MARKETING_ANALYST", + "QA_ENGINEER", + "PROJECT_MANAGER", + "PRODUCT_OWNER", + "USER") .requestMatchers(SecurityEndpoints.OPS.getMatchers()) - .hasAnyAuthority( - "SUPER_ADMIN", "ADMIN", "SENIOR_DATA_ENGINEER", "DATA_ENGINEER") + .hasAnyRole( + "SUPER_ADMIN", + "SYSTEM_ADMIN", + "WORKFLOW_ADMIN", + "OPERATIONS_MANAGER", + "DEVOPS", + "TECH_LEAD") .requestMatchers(SecurityEndpoints.USER.getMatchers()) - .authenticated() + .hasAnyRole("SUPER_ADMIN", "SYSTEM_ADMIN", "ORG_ADMIN", "USER") .anyRequest() .authenticated()) .formLogin(AbstractHttpConfigurer::disable) .logout( logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login").permitAll()) - .csrf(AbstractHttpConfigurer::disable) // API 사용을 위해 CSRF 비활성화 + .csrf(AbstractHttpConfigurer::disable) .build(); } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java index c73f462d..bc6eafe2 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java @@ -1,16 +1,7 @@ package com.gltkorea.icebang.config.security.endpoints; public enum SecurityEndpoints { - PUBLIC( - "/", - "/login", - "/register", - "/api/public/**", - "/health", - "/css/**", - "/js/**", - "/images/**", - "/v0/**"), + PUBLIC("/", "/v0/auth/login", "/api/public/**", "/health", "/css/**", "/js/**", "/images/**"), // 데이터 관리 관련 엔드포인트 DATA_ADMIN("/admin/**", "/api/admin/**", "/management/**", "/actuator/**"), @@ -25,7 +16,7 @@ public enum SecurityEndpoints { OPS("/api/scheduler/**", "/api/monitoring/**"), // 일반 사용자 엔드포인트 - USER("/user/**", "/profile/**"); + USER("/user/**", "/profile/**", "/v0/auth/check-session"); private final String[] patterns; diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java index b498119e..f1cdd856 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java @@ -4,13 +4,19 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.web.bind.annotation.*; import com.gltkorea.icebang.common.dto.ApiResponse; import com.gltkorea.icebang.domain.auth.dto.LoginRequestDto; import com.gltkorea.icebang.domain.auth.dto.RegisterDto; +import com.gltkorea.icebang.domain.auth.model.AuthCredential; import com.gltkorea.icebang.domain.auth.service.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -29,11 +35,26 @@ public ApiResponse register(@Valid @RequestBody RegisterDto registerDto) { } @PostMapping("/login") - public ApiResponse login(@RequestBody LoginRequestDto request) { + public ApiResponse login( + @RequestBody LoginRequestDto request, HttpServletRequest httpRequest) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); Authentication auth = authenticationManager.authenticate(token); + + SecurityContextHolder.getContext().setAuthentication(auth); + + // 세션에 SecurityContext 저장 (중요!) + HttpSession session = httpRequest.getSession(true); + session.setAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + SecurityContextHolder.getContext()); + return ApiResponse.success(auth); } + + @GetMapping("/check-session") + public ApiResponse checkSession(@AuthenticationPrincipal AuthCredential user) { + return ApiResponse.success(user != null); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java index 439bdf38..ab4acc2e 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/model/AuthCredential.java @@ -1,6 +1,8 @@ package com.gltkorea.icebang.domain.auth.model; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -28,10 +30,27 @@ public class AuthCredential implements UserDetails { // roles -> Spring Security authority로 변환 private List roles; + // MyBatis GROUP_CONCAT 결과를 List으로 변환하는 setter + public void setRoles(String rolesString) { + if (rolesString != null && !rolesString.trim().isEmpty()) { + this.roles = Arrays.asList(rolesString.split(",")); + } else { + this.roles = new ArrayList<>(); + } + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles != null ? roles : new ArrayList<>(); + } + @Override public Collection getAuthorities() { - return roles.stream() - .map(SimpleGrantedAuthority::new) // "ROLE_USER", "ROLE_ADMIN" 이런 값 + return getRoles().stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.trim())) // ROLE_ prefix 추가 + 공백 제거 .collect(Collectors.toList()); } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java index 3343785c..e3268314 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthCredentialAdapter.java @@ -1,8 +1,5 @@ package com.gltkorea.icebang.domain.auth.service; -import java.util.Arrays; -import java.util.Collections; - import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -26,14 +23,6 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep throw new UsernameNotFoundException("User not found with email: " + email); } - // roles가 "ROLE_USER,ROLE_ADMIN" 형태의 문자열이라면 List로 변환 - if (user.getRoles() != null && !user.getRoles().isEmpty()) { - String rolesString = user.getRoles().get(0); // GROUP_CONCAT 결과는 첫 번째 요소에 있음 - user.setRoles(Arrays.asList(rolesString.split(","))); - } else { - user.setRoles(Collections.emptyList()); - } - return user; } } diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml index 278dfb11..e852951b 100644 --- a/apps/user-service/src/main/resources/application.yml +++ b/apps/user-service/src/main/resources/application.yml @@ -5,4 +5,5 @@ spring: active: develop mybatis: # Mapper XML 파일 위치 - mapper-locations: classpath:mapper/**/*.xml \ No newline at end of file + mapper-locations: classpath:mapper/**/*.xml + type-handlers-package: com.gltkorea.icebang.config.mybatis.typehandler \ No newline at end of file diff --git a/apps/user-service/src/main/resources/log4j2-develop.yml b/apps/user-service/src/main/resources/log4j2-develop.yml index d1afc02b..1b5c6e35 100644 --- a/apps/user-service/src/main/resources/log4j2-develop.yml +++ b/apps/user-service/src/main/resources/log4j2-develop.yml @@ -119,6 +119,13 @@ Configuration: # 6. 트랜잭션 로그 - DB 작업 추적 - name: org.springframework.transaction + level: DEBUG + additivity: "false" + AppenderRef: + - ref: console-appender + - ref: file-info-appender + + - name: com.gltkorea.icebang.domain.auth.mapper level: DEBUG additivity: "false" AppenderRef: diff --git a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml index a07b1d6f..0023c224 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml @@ -19,14 +19,15 @@ u.email, u.password, uo.status, - GROUP_CONCAT(r.name) as roles + GROUP_CONCAT(DISTINCT r.name) as roles FROM user u LEFT JOIN user_organization uo ON u.id = uo.user_id LEFT JOIN user_role ur ON uo.id = ur.user_organization_id LEFT JOIN role r ON ur.role_id = r.id WHERE u.email = #{email} - AND uo.status IN ('ACTIVE', 'PENDING') -- 활성 상태만 조회 + AND uo.status IN ('ACTIVE', 'PENDING') GROUP BY u.id, u.email, u.password, uo.status + LIMIT 1 From ef392985b9bb0f347942af34b1e6d55c3849c323 Mon Sep 17 00:00:00 2001 From: can019 Date: Tue, 9 Sep 2025 20:16:40 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20User=20identity=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20user=20=EC=A0=95=EB=B3=B4=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 6 +++- .../user/controller/UserController.java | 14 ++++++--- .../user/dto/UserProfileResponseDto.java | 30 +++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/dto/UserProfileResponseDto.java diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java index f1cdd856..39fba398 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/controller/AuthController.java @@ -44,7 +44,6 @@ public ApiResponse login( SecurityContextHolder.getContext().setAuthentication(auth); - // 세션에 SecurityContext 저장 (중요!) HttpSession session = httpRequest.getSession(true); session.setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, @@ -57,4 +56,9 @@ public ApiResponse login( public ApiResponse checkSession(@AuthenticationPrincipal AuthCredential user) { return ApiResponse.success(user != null); } + + @GetMapping("/permissions") + public ApiResponse getPermissions(@AuthenticationPrincipal AuthCredential user) { + return ApiResponse.success(user); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/controller/UserController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/controller/UserController.java index e6b07bce..534e9ba6 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/controller/UserController.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/controller/UserController.java @@ -1,13 +1,13 @@ package com.gltkorea.icebang.domain.user.controller; -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 org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; import com.gltkorea.icebang.common.dto.ApiResponse; +import com.gltkorea.icebang.domain.auth.model.AuthCredential; import com.gltkorea.icebang.domain.user.dto.CheckEmailRequest; import com.gltkorea.icebang.domain.user.dto.CheckEmailResponse; +import com.gltkorea.icebang.domain.user.dto.UserProfileResponseDto; import com.gltkorea.icebang.domain.user.service.UserService; import jakarta.validation.Valid; @@ -27,4 +27,10 @@ public ApiResponse checkEmailAvailable( return ApiResponse.success(CheckEmailResponse.builder().available(available).build(), message); } + + @GetMapping("/me") + public ApiResponse getUserProfile( + @AuthenticationPrincipal AuthCredential user) { + return ApiResponse.success(UserProfileResponseDto.from(user)); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/dto/UserProfileResponseDto.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/dto/UserProfileResponseDto.java new file mode 100644 index 00000000..9254ace7 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/dto/UserProfileResponseDto.java @@ -0,0 +1,30 @@ +package com.gltkorea.icebang.domain.user.dto; + +import java.math.BigInteger; +import java.util.List; + +import com.gltkorea.icebang.domain.auth.model.AuthCredential; + +import lombok.Getter; + +@Getter +public class UserProfileResponseDto { + + private final BigInteger id; + private final String email; + private final String name; + private final List roles; + private final String status; + + public UserProfileResponseDto(AuthCredential authCredential) { + this.id = authCredential.getId(); + this.email = authCredential.getEmail(); + this.name = authCredential.getEmail(); // name 필드가 없으면 email 사용 + this.roles = authCredential.getRoles(); + this.status = authCredential.getStatus(); + } + + public static UserProfileResponseDto from(AuthCredential authCredential) { + return new UserProfileResponseDto(authCredential); + } +}