
msa-sb-user
eureka-server
api-gateway
msa-sb-products

postman클라이언트 api 요청
-> api-gateway
-> (msa-sb-user 회원가입 로그인 로그아웃 기능) (msa-sb-products 서비스 , 상품목록확인)


✅ 토큰이 필요 없는 URL은 인증 없이 바로 통과
✅ 토큰이 있으면 검증 후 사용자 정보를 헤더에 추가하여 개별 서비스로 전달
✅ 토큰이 만료되었으면 Refresh Token을 통해 새로운 Access Token을 발급 가능
토큰이 필요 없는 요청은 그대로 서비스로 전달
AntPathMatcher matcher = new AntPathMatcher();
for(String path : FREE_PATHS) {
if (matcher.match(path, reqUrl)) {
return chain.filter(exchange); // 인증 없이 통과
}
}
✔ 로그인, 회원가입 등의 요청은 토큰 없이도 접근 가능하도록 예외 처리
✔ 그 외 요청은 JWT 검증 필요
JWT 인증 후 요청 헤더에 사용자 정보 추가
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token != null) {
try {
String email = jwtTokenProvider.getEmailFromToken(token);
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header("X-Auth-User", email).build()
).build()
).contextWrite(ReactiveSecurityContextHolder.withAuthentication(
new UsernamePasswordAuthenticationToken(new User(email, "", new ArrayList<>()), null, null)
));
} catch (ExpiredJwtException e) {
// 토큰이 만료되었을 경우 처리 로직
} catch (Exception e) {
throw new RuntimeException(e);
}
}
✔ 토큰을 검증하고 이메일을 추출하여 요청 헤더(X-Auth-User)에 추가
✔ 개별 서비스는 API Gateway가 전달한 이메일 정보를 기반으로 인증 가능
✔ JWT가 만료된 경우 Refresh Token을 통해 새로운 Access Token을 발급하는 로직도 추가 가능
✅ JWT 인증 필터 적용 (JwtFilter)
✅ CORS, CSRF 비활성화
✅ 특정 URL에 대한 접근 권한 설정 (permitAll, authenticated)
✅ 인증/권한 오류(401, 403) 예외 처리
인증 없이 접근 가능한 URL 설정
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec
.pathMatchers("/",
"/auth/login", // 로그인
"/user/signup", // 회원가입
"/user/valid") // 이메일 인증
.permitAll()
.anyExchange().authenticated()) // 나머지 요청은 인증 필요
✔ 로그인, 회원가입, 이메일 인증 API는 인증 없이 접근 가능
✔ 그 외 요청은 JWT 인증이 필요
JWT 필터 적용
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHORIZATION)
✔ JWT 인증 필터(JwtFilter)를 등록하여 모든 요청을 검증
✔ 토큰이 유효하면 사용자 정보를 설정하고, 개별 서비스로 전달
인증 및 권한 오류 처리 (401, 403)
.exceptionHandling(exception -> {
exception.accessDeniedHandler(new CustomAccessDeniedHandler()) // 403
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()); // 401
})
✔ 접근 거부(403): 권한이 없는 유저가 관리자 페이지 접근 시
✔ 인증 오류(401): 로그인 없이 보호된 API에 접근 시

✔ msa-sb-products 라우트등록


✅ 이메일 인증을 위한 토큰 검증 및 계정 활성화 처리
✅ 게이트웨이에서 인증 없이 접근 가능하도록 설정 필요
회원가입 API
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody UserDto userDto) {
userService.createUser(userDto);
return ResponseEntity.ok("회원가입 성공");
}
✔ 회원가입 요청을 처리하고 UserService.createUser()를 호출하여 회원 데이터 저장
✔ 이메일 인증을 위한 메일 전송
이메일 인증 API
@GetMapping("/valid")
public ResponseEntity<String> valid(@RequestParam("token") String token) {
try {
userService.updateActivate(token);
return ResponseEntity.ok("이메일 인증 완료. 계정이 활성화 되었습니다.");
} catch (IllegalArgumentException e) {
return ResponseEntity.status(500).body("서버측 내부 오류 : " + e.getMessage());
}
}
✔ 토큰을 받아 userService.updateActivate(token)을 호출하여 계정 활성화
✔ 토큰이 조작되었거나 만료된 경우 예외 처리
✅ 사용자가 로그인하면 AuthService를 통해 인증을 수행하고 JWT 토큰을 발급
✅ 로그아웃 시 Redis에서 토큰을 삭제하여 무효화
✅ API Gateway를 통해 로그인/로그아웃 요청이 전달됨
로그인 API
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginReqDto loginReqDto,
HttpServletResponse response) {
return ResponseEntity.ok(authService.login(loginReqDto, response));
}
✔ 사용자가 loginReqDto(이메일, 비밀번호)를 입력하여 로그인 요청
✔ AuthService.login()에서 인증을 수행하고 JWT 토큰을 발급
✔ 발급된 JWT를 클라이언트에게 반환
로그아웃 API
@PostMapping("/logout")
public ResponseEntity<String> logout(@RequestHeader("X-Auth-User") String email,
@RequestHeader("Authorization") String accessToken) {
authService.logout(email, accessToken);
return ResponseEntity.ok("로그아웃 성공");
}
✔ 헤더에서 X-Auth-User(이메일)과 Authorization(JWT 토큰) 값을 받아 로그아웃 수행
✔ Redis에서 해당 토큰을 삭제하여 무효화
✅ 회원 가입 시 이메일 중복 검사 및 데이터 저장
✅ 이메일 인증을 위한 토큰 생성 및 Redis 저장
✅ 이메일 인증 완료 시 계정 활성화 (enable: false → true)
✅ 이메일 인증 후 Redis에서 인증 토큰 삭제
이메일 인증 후 계정 활성화 로직
public void updateActivate(String token) {
// 1. Redis에서 토큰을 조회하여 이메일 획득
String email = (String) redisTemplate.opsForValue().get(token);
// 2. 토큰이 존재하지 않으면 잘못된 토큰 또는 만료된 토큰이므로 예외 처리
if (email == null) {
throw new IllegalArgumentException("잘못된 토큰 혹은 만료된 토큰");
}
// 3. 이메일을 기반으로 사용자 정보 조회
UserEntity userEntity = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("사용자 오류(존재x)"));
// 4. 계정 활성화 (enable: false → true)
userEntity.setEnable(true);
userRepository.save(userEntity);
// 5. 인증이 완료되었으므로 Redis에서 해당 토큰 삭제
redisTemplate.delete(token);
}
✔ Redis에서 토큰을 조회하여 이메일을 획득
✔ 토큰이 유효하지 않으면 예외 발생 (잘못된 토큰 혹은 만료된 토큰)
✔ 이메일을 기반으로 DB에서 사용자 정보 조회 (userRepository.findByEmail(email))
✔ 계정 활성화 (enable: false → true) 후 DB에 저장
✔ 이메일 인증 완료 후 Redis에서 해당 인증 토큰 삭제
✅ 로그인 시 이메일 & 비밀번호 검증 후 JWT(Access/Refresh Token) 발급
✅ Refresh Token이 존재하지 않으면 새로 생성하여 Redis에 저장
✅ 로그아웃 시 Redis에서 Refresh Token 삭제
로그인 처리 (login)
public String login(LoginReqDto loginReqDto, HttpServletResponse response) {
String email = loginReqDto.getEmail();
String password = loginReqDto.getPassword();
try {
UserEntity userEntity = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("Email not found"));
if (!passwordEncoder.matches(password, userEntity.getPassword())) {
throw new IllegalArgumentException("비밀번호 불일치");
}
String accessToken = jwtTokenProvider.createAccessToken(email, password);
String refreshToken = tokenService.getRefreshToken(email);
if (refreshToken == null) {
refreshToken = jwtTokenProvider.createRefreshToken();
tokenService.saveRefreshToken(email, refreshToken);
}
response.addHeader("RefreshToken", refreshToken);
response.addHeader("accessToken", accessToken);
response.addHeader("X-Auth-User", email);
} catch (Exception e) {
System.out.println("로그인시 오류 발생: " + e.getMessage());
return "로그인 실패";
}
return "로그인 성공";
}
✔ 이메일 & 비밀번호 검증 후 JWT Access Token & Refresh Token 발급
✔ Refresh Token은 Access Token이 없거나 만료되었을 때, Refresh Token을 사용하여 새로운 Access Token을 발급받도록 함
✔ Refresh Token이 없으면 새로 생성하여 Redis에 저장
✔ Access Token, Refresh Token을 응답 헤더에 추가하여 반환
로그아웃 처리 (logout)
public void logout(String email, String accessToken) {
if (!jwtTokenProvider.validateToken(accessToken)) {
throw new IllegalArgumentException("부적절한 토큰");
}
tokenService.deleteRefreshToken(email);
}
✔ Access Token의 유효성 검증 (validateToken)
✔ 로그아웃 시 Redis에서 Refresh Token 삭제 → 모든 기기에서 로그아웃 가능
✅ Refresh Token을 Redis에서 이메일을 키로 하여 저장 및 조회
✅ Refresh Token의 유효기간을 7일로 설정
✅ 로그아웃 시 Redis에서 해당 Refresh Token 삭제
Refresh Token 조회 (getRefreshToken)
public String getRefreshToken(String email) {
return redisTemplate.opsForValue().get(email); // 키:email, 값:토큰
}
✔ Redis에서 이메일을 키로 사용하여 Refresh Token을 조회
Refresh Token 저장 (saveRefreshToken)
public void saveRefreshToken(String email, String refreshToken) {
redisTemplate.opsForValue().set(email, refreshToken, Duration.ofDays(7)); // 만료시간 7일
}
✔ 이메일을 키로 하여 Refresh Token을 Redis에 저장
✔ 유효기간을 7일로 설정하여 자동 만료 처리
Refresh Token 삭제 (deleteRefreshToken)
public void deleteRefreshToken(String email) {
redisTemplate.delete(email);
}
✔ 로그아웃 시 해당 이메일의 Refresh Token을 Redis에서 삭제
✔ 로그아웃하면 기존 Refresh Token이 무효화되므로 보안 강화
✅ 클라이언트에서 로그인 요청 시 JSON 데이터를 객체로 변환
✅ 이메일, 사용자명, 비밀번호 필드를 포함
✅ @Data 어노테이션을 사용하여 Getter, Setter, toString 자동 생성
로그인 요청 데이터 구조
private String email;
private String userName;
private String password;
✔ 클라이언트가 입력한 이메일, 사용자명, 비밀번호를 저장
✔ 이메일과 비밀번호를 기반으로 로그인 검증 진행
✔ userName 필드는 로그인 시 필요하지 않으므로 제거 가능
✅ Access Token & Refresh Token 생성 (만료시간 적용)
✅ 토큰 서명 및 검증 (validateToken)
✅ 토큰 내부에서 이메일 및 역할(Role) 정보 저장
Access Token & Refresh Token 생성
public String createAccessToken(String email, String role) {
return createToken(email, role, accessTokenExpiration); // 1시간 만료시간
}
public String createRefreshToken() {
return createToken(null, null, refreshTokenExpiration); // 7일 만료시간
}
✔ Access Token은 email, role 정보를 포함하고 1시간 만료
✔ Refresh Token은 별다른 정보 없이 7일 만료
토큰 생성 (createToken)
public String createToken(String email, String role, long expiration) {
Map<String, Object> claims = new HashMap<>();
if (email != null) claims.put("email", email);
if (role != null) claims.put("role", role);
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}
✔ 이메일 & 역할(Role) 정보를 포함하여 JWT 생성
✔ 토큰 만료시간 설정 (Access Token: 1시간, Refresh Token: 7일)
✔ HS256 알고리즘을 사용하여 서명 적용
토큰 검증 (validateToken)
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
✔ 토큰을 해석하여 유효성 검사
✔ 서명이 유효하고 만료되지 않았다면 true 반환
✔ 유효하지 않으면 false 반환 (조작되었거나 만료된 토큰)


✅ Spring Security + JWT 인증 처리
✅ Eureka Client & Redis 연동
✅ JPA & MySQL 데이터베이스 설정
✅ Spring Web (MVC 기반 서비스)
의존성
dependencies {
// 편의성 도구 devtool, lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// 스프링시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-crypto'
// JWT 처리, 버전은 참고
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
// mysql -> 회원정보 저장 (이메일, 아이디, 비번, 이름(?),..)
runtimeOnly 'com.mysql:mysql-connector-j'
// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 유레카 클라이언트
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// redis, 토큰 저장 및 조회
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// 리액티브 웹(spring-boot-starter-webflux) <-> MVC 웹(spring-boot-starter-web)
implementation 'org.springframework.boot:spring-boot-starter-web'
// 향후 추가
// kafka : 주문, 결제 등등 이벤트 발생시 송수신 처리 등등 비동기 구성 -> 메세징 서비스
// 서비스간 통신용도
// (*)Jackson | gson : JSON 데이터 처리 -> 데이터 형태 파싱
// actuator
// 모니터링, 매트릭수집(성능측정), 환경정보, 로그관리, 헬스 체크,..
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Fegin Client
// Spring Cloud에서 제공하는 http 클라이언트
// restful client 호출때 간편 사용을 위한 서포트 라이브러리
// msa 내부에서 서비스간 통신시 활용됨(많이 사용)
// 로그 -> 롬복 지원 가능 -> AOP 연동
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

이 패키지는 클라이언트의 HTTP 요청을 처리하는 컨트롤러를 포함합니다.
✅ /pdts 경로로 들어오는 요청을 처리
✅ 전체 상품 목록을 조회하는 GET API 제공
✅ ProductsService를 호출하여 상품 데이터를 가져옴
전체 상품 목록 조회 API
@GetMapping
public ResponseEntity<List> allProducts() {
return ResponseEntity.ok(productsService.allProducts());
}
✔ HTTP GET /pdts 요청 시 전체 상품 목록을 반환
✔ ProductsService.allProducts()를 호출하여 데이터를 가져옴
✔ ResponseEntity.ok()를 사용하여 HTTP 200 응답과 함께 데이터를 반환

이 패키지는 클라이언트와 데이터를 주고받기 위한 DTO(Data Transfer Object)를 포함합니다.
✅ 상품 이름(pdtName)과 가격(pdtPrice)만 포함
✅ 추가적인 상세 정보는 포함하지 않고, 상세보기에서 노출 가능
✅ @Builder 패턴을 사용하여 객체 생성 시 유연성을 제공
상품 정보 DTO 구조
@Data
public class ProductDto {
private String pdtName;
private Integer pdtPrice;
@Builder
public ProductDto(String pdtName, Integer pdtPrice) {
this.pdtName = pdtName;
this.pdtPrice = pdtPrice;
}
}
✔ 상품 이름과 가격만 포함하여 기본 정보 제공
✔ @Data 어노테이션으로 Getter, Setter, toString 자동 생성
✔ @Builder 어노테이션을 사용하여 객체 생성 시 가독성 향상

이 패키지는 데이터베이스와 매핑되는 JPA 엔티티(Entity) 클래스를 포함합니다.
✅ 상품 ID(pdtId), 상품명(pdtName), 가격(pdtPrice), 수량(pdtQuantity) 포함
✅ 자동 증가 ID (@GeneratedValue) 적용
✅ JSON 응답 시 pdtId 필드는 제외 (@JsonIgnore)
데이터베이스 매핑 (@Entity, @Table)
@Entity
@Table(name="products")
public class ProductEntity {
✔ 이 클래스는 products 테이블과 연결됨
✔ Spring Data JPA를 통해 자동으로 DB 연동 가능
기본 필드 정의
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonIgnore
private String pdtId;
private String pdtName;
private Integer pdtPrice;
private Integer pdtQuantity;
✔ pdtId는 기본키(@Id)이며, 자동 증가 설정(@GeneratedValue) 적용
✔ @JsonIgnore를 사용하여 pdtId를 JSON 응답에서 제외
✔ 상품명(pdtName), 가격(pdtPrice), 수량(pdtQuantity) 포함

이 패키지는 데이터베이스와 직접적으로 연결되어 CRUD(Create, Read, Update, Delete) 작업을 수행하는 역할을 합니다.

이 패키지는 비즈니스 로직을 처리하는 서비스 계층을 포함합니다.
✅ ProductsRepository를 통해 데이터베이스에서 상품 목록을 조회
✅ 조회된 ProductEntity 데이터를 ProductDto로 변환하여 반환
전체 상품 목록 조회 (allProducts 메서드)
public List<ProductDto> allProducts() {
List<ProductEntity> pdts = productsRepository.findAll();
return pdts.stream()
.map(p -> ProductDto.builder()
.pdtName(p.getPdtName())
.pdtPrice(p.getPdtPrice())
.build())
.collect(Collectors.toList());
}
✔ findAll()을 사용하여 모든 상품 정보를 조회
✔ 조회된 ProductEntity 리스트를 ProductDto로 변환
✔ Stream API를 활용하여 pdtName과 pdtPrice만 포함한 DTO 리스트 생성













