Rookies-2025.02.05

이주원·2025년 2월 5일

sk쉴더스 루키즈

목록 보기
5/36

db삭제

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

오늘 실습한 내용

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

api-gateway

jwt

jwtfilter 업데이트

✅ 토큰이 필요 없는 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을 발급하는 로직도 추가 가능


config

secureconfig 업데이트

✅ 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에 접근 시


main함수

ApiGatewayApplication 업데이트

✔ msa-sb-products 라우트등록


msa-sb-user


controller

1. usercontroller.java 업데이트

✅ 이메일 인증을 위한 토큰 검증 및 계정 활성화 처리
✅ 게이트웨이에서 인증 없이 접근 가능하도록 설정 필요


회원가입 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)을 호출하여 계정 활성화
✔ 토큰이 조작되었거나 만료된 경우 예외 처리





2. AuthController 추가

✅ 사용자가 로그인하면 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에서 해당 토큰을 삭제하여 무효화

service

1. userservice 업데이트

✅ 회원 가입 시 이메일 중복 검사 및 데이터 저장
✅ 이메일 인증을 위한 토큰 생성 및 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에서 해당 인증 토큰 삭제

2. AuthService 추가

✅ 로그인 시 이메일 & 비밀번호 검증 후 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 삭제 → 모든 기기에서 로그아웃 가능

3. TokenService 추가

✅ 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이 무효화되므로 보안 강화

Dto

LoginReqDto 추가

✅ 클라이언트에서 로그인 요청 시 JSON 데이터를 객체로 변환
✅ 이메일, 사용자명, 비밀번호 필드를 포함
✅ @Data 어노테이션을 사용하여 Getter, Setter, toString 자동 생성


로그인 요청 데이터 구조

  private String email;
  private String userName;
  private String password;

✔ 클라이언트가 입력한 이메일, 사용자명, 비밀번호를 저장
✔ 이메일과 비밀번호를 기반으로 로그인 검증 진행
✔ userName 필드는 로그인 시 필요하지 않으므로 제거 가능

jwt

jwttokenprovide 추가

✅ 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 반환 (조작되었거나 만료된 토큰)


msa-sb-products

build.gradle

dependencies

✅ 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'
  }

controller

이 패키지는 클라이언트의 HTTP 요청을 처리하는 컨트롤러를 포함합니다.

ProductsController

✅ /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

이 패키지는 클라이언트와 데이터를 주고받기 위한 DTO(Data Transfer Object)를 포함합니다.

ProductDto

✅ 상품 이름(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 어노테이션을 사용하여 객체 생성 시 가독성 향상


entity


이 패키지는 데이터베이스와 매핑되는 JPA 엔티티(Entity) 클래스를 포함합니다.

ProductEntity

✅ 상품 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) 포함


repository

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


service

이 패키지는 비즈니스 로직을 처리하는 서비스 계층을 포함합니다.

ProductsService

✅ 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 리스트 생성









모니터링

포스트맨에 구글계정으로 가입했습니다.

썬더 클라이언트와 비슷합니다. 장점은 다른 피시에서도 로그인만하면 사용가능 합니다.

postman- agent를 설치하면 앱에서 pc의 server로 들어올 수 있도록 도와줌

설치완료

이메일 인증기능 테스트를 해보았습니다.

회원가입 성공



회원가입 후 토큰이 생겨 있습니다



이메일 인증 요청을 보냅니다.

토큰이 사라졌습니다. 성공 짝짝짝





이번에는 로그인 테스트를 진행합니다.

로그인성공



로그인성공후 Headers 를 확인해보니 토큰이 제대로 들어와있습니다.

user메일, 토큰값을 전달하여 로그아웃도 성공

로그인할때 생겼던 이메일이 로그아웃 후에 없어졌습니다.

새로만든 서비스(MSA-SB-USER)를 게이트웨이에 추가해줬습니다.

끝.

profile
뭐가될지 모름

0개의 댓글