mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-12 20:49:09 +00:00
release
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
}
|
||||
bootJar {
|
||||
archiveFileName = "member.jar"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: member-service
|
||||
datasource:
|
||||
url: ${POSTGRES_URL}
|
||||
username: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
jwt:
|
||||
secret-key: ${JWT_SECRET_KEY}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400000}
|
||||
|
||||
allowedorigins: ${ALLOWED_ORIGINS:*}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
package com.unicorn.lifesub.member;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MemberApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MemberApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// File: lifesub/member/src/main/java/com/unicorn/lifesub/member/config/InitialDataLoader.java
|
||||
package com.unicorn.lifesub.member.config;
|
||||
|
||||
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
|
||||
import com.unicorn.lifesub.member.repository.jpa.MemberRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InitialDataLoader implements CommandLineRunner {
|
||||
private final MemberRepository memberRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
// 기존 사용자 데이터가 없을 경우에만 초기 데이터 생성
|
||||
if (memberRepository.count() == 0) {
|
||||
Set<String> userRoles = new HashSet<>();
|
||||
userRoles.add("USER");
|
||||
String encodedPassword = passwordEncoder.encode("P@ssw0rd$");
|
||||
|
||||
IntStream.rangeClosed(1, 10).forEach(i -> {
|
||||
String userId = String.format("user%02d", i);
|
||||
MemberEntity member = MemberEntity.builder()
|
||||
.userId(userId)
|
||||
.userName("사용자" + i)
|
||||
.password(encodedPassword)
|
||||
.roles(userRoles)
|
||||
.build();
|
||||
|
||||
memberRepository.save(member);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.unicorn.lifesub.member.config;
|
||||
|
||||
import com.unicorn.lifesub.member.config.jwt.CustomUserDetailsService;
|
||||
import com.unicorn.lifesub.member.config.jwt.JwtTokenProvider;
|
||||
import com.unicorn.lifesub.member.config.jwt.JwtAuthenticationFilter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
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.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;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@SuppressWarnings("unused")
|
||||
public class SecurityConfig {
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final CustomUserDetailsService customUserDetailsService;
|
||||
|
||||
@Value("${allowedorigins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
public SecurityConfig(JwtTokenProvider jwtTokenProvider, CustomUserDetailsService customUserDetailsService) {
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
this.customUserDetailsService = customUserDetailsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||
return authConfig.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors
|
||||
.configurationSource(corsConfigurationSource())
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(HttpMethod.GET, "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.userDetailsService(customUserDetailsService)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.unicorn.lifesub.member.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("unused")
|
||||
@SecurityScheme(
|
||||
name = "bearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
bearerFormat = "JWT",
|
||||
scheme = "bearer"
|
||||
)
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("회원 서비스 API")
|
||||
.version("v1.0.0")
|
||||
.description("회원 서비스 API 명세서입니다."));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.unicorn.lifesub.member.config.jwt;
|
||||
|
||||
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
|
||||
import com.unicorn.lifesub.member.repository.jpa.MemberRepository;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
public CustomUserDetailsService(MemberRepository memberRepository) {
|
||||
this.memberRepository = memberRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
|
||||
MemberEntity member = memberRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with userId: " + userId));
|
||||
|
||||
Set<GrantedAuthority> authorities = member.getRoles().stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return User.builder()
|
||||
.username(member.getUserId())
|
||||
.password(member.getPassword())
|
||||
.authorities(authorities)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.lifesub.member.config.jwt;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String token = jwtTokenProvider.resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.validateToken(token) == 1) {
|
||||
Authentication authentication = jwtTokenProvider.getAuthentication(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.unicorn.lifesub.member.config.jwt;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
|
||||
import com.auth0.jwt.exceptions.InvalidClaimException;
|
||||
import com.auth0.jwt.exceptions.SignatureVerificationException;
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.unicorn.lifesub.common.exception.ErrorCode;
|
||||
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
|
||||
import com.unicorn.lifesub.common.exception.InfraException;
|
||||
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
private final Algorithm algorithm;
|
||||
private final long accessTokenValidityInMilliseconds;
|
||||
private final long refreshTokenValidityInMilliseconds;
|
||||
|
||||
public JwtTokenProvider(
|
||||
@Value("${jwt.secret-key}") String secretKey,
|
||||
@Value("${jwt.access-token-validity}") long accessTokenValidityInMilliseconds,
|
||||
@Value("${jwt.refresh-token-validity}") long refreshTokenValidityInMilliseconds) {
|
||||
this.algorithm = Algorithm.HMAC512(secretKey);
|
||||
this.accessTokenValidityInMilliseconds = accessTokenValidityInMilliseconds * 1000;
|
||||
this.refreshTokenValidityInMilliseconds = refreshTokenValidityInMilliseconds * 1000;
|
||||
}
|
||||
|
||||
public JwtTokenDTO createToken(MemberEntity memberEntity, Collection<? extends GrantedAuthority> authorities) {
|
||||
try {
|
||||
Date now = new Date();
|
||||
Date accessTokenValidity = new Date(now.getTime() + accessTokenValidityInMilliseconds);
|
||||
Date refreshTokenValidity = new Date(now.getTime() + refreshTokenValidityInMilliseconds);
|
||||
|
||||
String accessToken = JWT.create()
|
||||
.withSubject(memberEntity.getUserId())
|
||||
.withClaim("userId", memberEntity.getUserId())
|
||||
.withClaim("userName", memberEntity.getUserName())
|
||||
.withClaim("auth", authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
|
||||
.withIssuedAt(now)
|
||||
.withExpiresAt(accessTokenValidity)
|
||||
.sign(algorithm);
|
||||
|
||||
String refreshToken = JWT.create()
|
||||
.withSubject(memberEntity.getUserId())
|
||||
.withIssuedAt(now)
|
||||
.withExpiresAt(refreshTokenValidity)
|
||||
.sign(algorithm);
|
||||
|
||||
return JwtTokenDTO.builder()
|
||||
.accessToken(accessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.build();
|
||||
} catch(Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(algorithm).build();
|
||||
verifier.verify(refreshToken);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserIdFromToken(String refreshToken) {
|
||||
try {
|
||||
DecodedJWT decodedJWT = JWT.decode(refreshToken);
|
||||
return decodedJWT.getSubject();
|
||||
} catch (Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveToken(HttpServletRequest request) {
|
||||
try {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
public int validateToken(String token) {
|
||||
log.info("******** validateToken: {}", token);
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(algorithm).build();
|
||||
verifier.verify(token);
|
||||
return 1; // 검사 성공 시 1 반환
|
||||
} catch (TokenExpiredException e) {
|
||||
log.error("Token validation failed: {}", e.getMessage(), e);
|
||||
throw new InfraException(ErrorCode.TOKEN_EXPIRED);
|
||||
} catch (SignatureVerificationException e) {
|
||||
log.error("Token validation failed: {}", e.getMessage(), e);
|
||||
throw new InfraException(ErrorCode.SIGNATURE_VERIFICATION_EXCEPTION);
|
||||
} catch (AlgorithmMismatchException e) {
|
||||
log.error("AlgorithmMismatchException: {}", e.getMessage(), e);
|
||||
throw new InfraException(ErrorCode.ALGORITHM_MISMATCH_EXCEPTION);
|
||||
} catch (InvalidClaimException e) {
|
||||
log.error("InvalidClaimException: {}", e.getMessage(), e);
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
} catch (Exception e) {
|
||||
log.error("Undefined Error: {}", e.getMessage(), e);
|
||||
throw new InfraException(ErrorCode.UNDIFINED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public Authentication getAuthentication(String token) {
|
||||
try {
|
||||
DecodedJWT decodedJWT = JWT.decode(token);
|
||||
String username = decodedJWT.getSubject();
|
||||
String[] authStrings = decodedJWT.getClaim("auth").asArray(String.class);
|
||||
Collection<? extends GrantedAuthority> authorities = Arrays.stream(authStrings)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
UserDetails userDetails = new User(username, "", authorities);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
|
||||
} catch (Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.unicorn.lifesub.member.controller;
|
||||
|
||||
import com.unicorn.lifesub.common.dto.ApiResponse;
|
||||
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
|
||||
import com.unicorn.lifesub.member.dto.*;
|
||||
import com.unicorn.lifesub.member.service.MemberService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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;
|
||||
|
||||
@Tag(name = "회원 API", description = "회원 인증 관련 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class MemberController {
|
||||
private final MemberService memberService;
|
||||
|
||||
@Operation(summary = "로그인", description = "사용자 ID와 비밀번호로 로그인합니다.")
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<JwtTokenDTO>> login(@Valid @RequestBody LoginRequest request) {
|
||||
JwtTokenDTO response = memberService.login(request);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@Operation(summary = "로그아웃", description = "현재 로그인된 사용자를 로그아웃합니다.")
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<ApiResponse<LogoutResponse>> logout(@Valid @RequestBody LogoutRequest request) {
|
||||
LogoutResponse response = memberService.logout(request);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.unicorn.lifesub.member.domain;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
public class Member {
|
||||
private final String userId;
|
||||
private final String userName;
|
||||
private final String password;
|
||||
private final Set<String> roles;
|
||||
|
||||
@Builder
|
||||
public Member(String userId, String userName, String password, Set<String> roles) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.unicorn.lifesub.member.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class LoginRequest {
|
||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
||||
private String userId;
|
||||
|
||||
@NotBlank(message = "비밀번호는 필수입니다.")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.unicorn.lifesub.member.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class LogoutRequest {
|
||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
||||
private String userId;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.unicorn.lifesub.member.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class LogoutResponse {
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.unicorn.lifesub.member.repository.entity;
|
||||
|
||||
import com.unicorn.lifesub.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.lifesub.member.domain.Member;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "members")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class MemberEntity extends BaseTimeEntity {
|
||||
@Id
|
||||
private String userId;
|
||||
|
||||
private String userName;
|
||||
private String password;
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "member_roles", joinColumns = @JoinColumn(name = "user_id"))
|
||||
@Column(name = "role")
|
||||
private Set<String> roles = new HashSet<>();
|
||||
|
||||
@Builder
|
||||
public MemberEntity(String userId, String userName, String password, Set<String> roles) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public Member toDomain() {
|
||||
return Member.builder()
|
||||
.userId(userId)
|
||||
.userName(userName)
|
||||
.password(password)
|
||||
.roles(roles)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MemberEntity fromDomain(Member member) {
|
||||
return MemberEntity.builder()
|
||||
.userId(member.getUserId())
|
||||
.userName(member.getUserName())
|
||||
.password(member.getPassword())
|
||||
.roles(member.getRoles())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.unicorn.lifesub.member.repository.jpa;
|
||||
|
||||
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MemberRepository extends JpaRepository<MemberEntity, String> {
|
||||
Optional<MemberEntity> findByUserId(String userId);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.unicorn.lifesub.member.service;
|
||||
|
||||
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
|
||||
import com.unicorn.lifesub.member.dto.*;
|
||||
|
||||
public interface MemberService {
|
||||
JwtTokenDTO login(LoginRequest request);
|
||||
LogoutResponse logout(LogoutRequest request);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.unicorn.lifesub.member.service;
|
||||
|
||||
import com.unicorn.lifesub.common.exception.BusinessException;
|
||||
import com.unicorn.lifesub.common.exception.ErrorCode;
|
||||
import com.unicorn.lifesub.member.config.jwt.JwtTokenProvider;
|
||||
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
|
||||
import com.unicorn.lifesub.member.dto.LoginRequest;
|
||||
import com.unicorn.lifesub.member.dto.LogoutRequest;
|
||||
import com.unicorn.lifesub.member.dto.LogoutResponse;
|
||||
import com.unicorn.lifesub.common.exception.InfraException;
|
||||
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
|
||||
import com.unicorn.lifesub.member.repository.jpa.MemberRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MemberServiceImpl implements MemberService {
|
||||
private final MemberRepository memberRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public JwtTokenDTO login(LoginRequest request) {
|
||||
MemberEntity member = memberRepository.findByUserId(request.getUserId())
|
||||
.orElseThrow(() -> new InfraException(ErrorCode.MEMBER_NOT_FOUND));
|
||||
|
||||
// 사용자의 권한 정보 생성
|
||||
Collection<? extends GrantedAuthority> authorities = member.getRoles().stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
return jwtTokenProvider.createToken(member, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public LogoutResponse logout(LogoutRequest request) {
|
||||
// 실제 구현에서는 Redis 등을 사용하여 토큰 블랙리스트 관리
|
||||
return LogoutResponse.builder()
|
||||
.message("로그아웃이 완료되었습니다.")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: member-service
|
||||
datasource:
|
||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:member}
|
||||
username: ${POSTGRES_USER:postgres}
|
||||
password: ${POSTGRES_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
jwt:
|
||||
secret-key: ${JWT_SECRET_KEY}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400000}
|
||||
|
||||
allowedorigins: ${ALLOWED_ORIGINS:*}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
Reference in New Issue
Block a user