mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-13 13:09:10 +00:00
release
This commit is contained in:
@@ -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