[12.12] 내일배움캠프[Spring] TIL-30

박상훈·2022년 12월 13일
0

내일배움캠프[TIL]

목록 보기
30/72

[12.12] 내일배움캠프[Spring] TIL-30

1. 로그인 / 회원가입

  • 지난 포스팅 : 네이버 검색 API를 활용하여 상품을 키워드로 검색하고, 최저가를 등록하는 작업까지 진행
  • 오늘은 각 회원별로 이용자/관리자를 나눠 회원가입을 진행
  • 이용자 -> 자신이 선택한 최저가 등록 상품만 볼 수 있음.
  • 관리자 -> 모든 이용자가 등록한 최저가 등록 상품을 볼 수 있음.
  • 비 로그인 -> 검색은 가능하지만 최저가 등록을 할 수 없음.

인증,인가

  • 회원정보를 입력받아 회원 가입을 진행한다.
  • 로그인 : 그 회원이 현재 등록된 회원인지 인증한다.
  • 인가 : 등록 된 회원이 접근할 수 있는 리소스를 인가해준다.

인증( Authentication )

  • 해당 유저가 실제 존재하는지 확인하는 작업

인증 방식

쿠키 - 세션 인증 방식

  • 쿠키 - 세션 방식은 서버가 현재 특정 유저가 로그인 되어있다 라는 상태를 저장하는 방식이다.
  • 인증과 관련된 약간의 정보를 서버가 가지고 있는 개념.
    1) 사용자가 로그인 요청을 보낸다.
    2) 서버는 DB를 뒤져서 아이디와 패스워드가 맞는지 전부 확인한다.
    3) 실제 유저 DB와 정보가 일치한다면, 인증을 통과한 것으로 보고, 세션 저장소에 해당 유저가 로그인 되었다는 정보를 넣는다.
    4) 세션 저장소에는 유저의 정보와는 상관없는 난수인 Session Id를 발급한다.
    5) 서버는 로그인 요청에 대한 응답으로 Session Id를 내어준다.
    6) 사용자는 이 Session Id를 쿠키에 가지고 있다가, 앞으로 요청 마다 HttpHeader에 이 값을 같이 넣어서 보내준다.
    7) 클라이언트의 요청에서 쿠키 값을 발견 했다면, 서버는 저장된 세션 저장소에서 쿠키를 검증한다.

세션

  • Http는 상태를 저장하지 않는다.
  • 따라서 1번 요청과 2번 요청은 같은 요청이지만, 같은 요청인지 인식하지 못한다.
  • 이 때 이 같은 요청에 대한 인식을 하고 그에 따른 작업의 반복을 줄이기 위해, 쿠키,세션의 개념이 등장한다.

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
  • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용됨

쿠키

1) 클라이언트에 저장 될 목적으로 생성한 작은 정보를 담은 파일
2) Name : 쿠키를 구별하는데 사용되는 키(중복될 수 없음)
3) Value : 쿠키의 값
4) Domain : 쿠키가 저장된 도메인
5) Path : 쿠키가 사용되는 경로
6) Expires : 쿠키의 만료기간

쿠키와 세션의 비교

-쿠키세션
설명클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
저장위치클라이언트 (웹 브라우져)웹 서버
사용 예사이트 팝업의 "오늘 다시보지 않기" 정보 저장로그인 정보 저장
만료 시점쿠키 저장 시 만료일시 설정 가능(브라우져 종료시도 유지 가능)다음 조건 중 하나가 만족될 경우 만료됨
1. 브라우져 종료 시까지
2. 클라이언트 로그아웃 시까지
3. 서버에 설정한 유지기간까지 해당 클라이언트의 재요청이 없는 경우
용량 제한브라우져 별로 다름 (크롬 기준)
- 하나의 도메인 당 180개
- 하나의 쿠키 당 4KB(=4096byte)
개수 제한 없음 (단, 세션 저장소 크기 이상 저장 불가능)
보안취약
(클라이언트에서 쿠키 정보를 쉽게 변경, 삭제 및 가로채기 당할 수 있음)
비교적 안전
(서버에 저장되기 때문에 상대적으로 안전)

JWT 기반 인증 방식

  • JWT ( Json Web Token )은 인증에 필요한 정보를 암호화하는 것.
  • 쿠키 - 세션 방식과 유사하게 JWT토근을 Http 헤더에 실어 서버가 클라이언트를 식별한다.
  • HEADER , VERIFY SIGNATURE : 암호화 방식
  • PAYLOAD : 유저의 정보
    1) 사용자가 로그인 요청을 보낸다
    2) 서버는 DB를 뒤져서 아이디와 비밀번호를 대조해본다.
    3) 실제 유저 테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화해서 내보낸다.
    4) 서버는 로그인 요청 응답으로 JWT토큰을 준다.
    5) 클라이언트는 그 토큰을 저장하고 앞으로 요청 시 토큰을 같이 전송한다.
    6) 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증한다.
    7) 이후에는 로그인 된 유저에 따른 정보를 내어준다.

  • 로그인 정보를 Server에 저장하지 않고, Client정보를 JWT로 암호화하여 저장
  • 모든 서버에서 동일한 Secret Key 보유

  • 장점 : 동시 접속자 부하를 낮춘다.
  • 단점 : 구현이 복잡하고, 한번 생성된 JWT를 일부만 만료시킬 수 없음

1) JWT 를 Client 응답에 전달
-> JWT 전달방법은 개발자가 정함

예) 응답 Header 에 아래 형태로 JWT 전달

Authorization: BEARER <JWT>

ex)Authorization: BEARER eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzcGFydGEiLCJVU0VSTkFNRSI6IuultO2DhOydtCIsIlVTRVJfUk9MRSI6IlJPTEVfVVNFUiIsIkVYUCI6MTYxODU1Mzg5OH0.9WTrWxCWx3YvaKZG14khp21fjkU1VjZV4e9VEf05Hok

  • Client 에서 JWT 저장 (쿠키, Local storage 등)

2) Client 에서 JWT 통해 인증방법
-> JWT 를 API 요청 시마다 Header 에 포함

예) HTTP Headers

Content-Type: application/json
Authorization: Bearer <JWT>
...
  • Server
  1. Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
  2. JWT 유효기간이 지나지 않았는지 검증
  3. 검증 성공시 : JWT → 에서 사용자 정보를 가져와 확인
  • JWT 는 누구나 평문으로 복호화 가능
  • 하지만 Secret Key 가 없으면 JWT 수정 불가능
  • 결국 JWT 는 Read only 데이터
  • JWT 정보 보기 링크 : https://jwt.io/
  • 이렇게 절차를 걸쳐 인증이 완료 되었다면, 회원 가입에 따른 로그인이 성공한다.
  • 그렇다면 로그인 후 그 정보가 유지 되어야 하며, 회원 / 관리자에 따른 권한이 부여되어야 한다.
  • 그걸 인가 라고 부른다.

JWT 구현하기

  • Dependency 추가
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
  • 내가 만들 서버의 JWT Key ( application.properties )
jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
  • JwtUtil.java
package com.sparta.myselectshop.jwt;


import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String AUTHORIZATION_KEY = "auth";
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 만료시간

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                .setSubject(username)
                .claim(AUTHORIZATION_KEY, role)
                .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

}

JWT 적용시키기

User API 명세 변경하기

기능MethodURLRequest
회원가입 페이지GET/api/user/signup--
회원가입POST/api/user/signupPOST Form 태그
{
"username" : String,
”password”:String,
”email : String,
”admin” : boolean,
”adminToken" : String
}
로그인 페이지GET/api/user/login--
로그인POST/api/user/signup{
"username" : String,
”password” : String
}
  • Enum.java
package com.sparta.myselectshop.entity;

public enum UserRoleEnum {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}
  • User.java
package com.sparta.myselectshop.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    public User(String username, String password, String email, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
    }

}
  • Product.java
package com.sparta.myselectshop.entity;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.naver.dto.ItemDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // ID가 자동으로 생성 및 증가합니다.
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String image;

    @Column(nullable = false)
    private String link;

    @Column(nullable = false)
    private int lprice;

    @Column(nullable = false)
    private int myprice;

    @Column(nullable = false)
    private Long userId;

    public Product(ProductRequestDto requestDto, Long userId) {
        this.title = requestDto.getTitle();
        this.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.myprice = 0;
        this.userId = userId;
    }

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }

    public void updateByItemDto(ItemDto itemDto) {
        this.lprice = itemDto.getLprice();
    }

}
  • ProductRepository.interface
package com.sparta.myselectshop.repository;

import com.sparta.myselectshop.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findAllByUserId(Long userId);
    Optional<Product> findByIdAndUserId(Long id, Long userId);
}
  • ProductController.java
package com.sparta.myselectshop.controller;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductController {

    private final ProductService productService;

    // 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, HttpServletRequest request) {
        // 응답 보내기
        return productService.createProduct(requestDto, request);
    }

    // 관심 상품 조회하기
    @GetMapping("/products")
    public List<ProductResponseDto> getProducts(HttpServletRequest request) {
        // 응답 보내기
        return productService.getProducts(request);
    }

    // 관심 상품 최저가 등록하기
    @PutMapping("/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto, HttpServletRequest request) {
        // 응답 보내기 (업데이트된 상품 id)
        return productService.updateProduct(id, requestDto, request);
    }

}
  • ProductService.java
package com.sparta.myselectshop.service;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.repository.UserRepository;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    @Transactional
    public ProductResponseDto createProduct(ProductRequestDto requestDto, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 추가 가능
        if (token != null) {
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 요청받은 DTO 로 DB에 저장할 객체 만들기
            Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));

            return new ProductResponseDto(product);
        } else {
            return null;
        }
    }

    @Transactional(readOnly = true)
    public List<ProductResponseDto> getProducts(HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 사용자 권한 가져와서 ADMIN 이면 전체 조회, USER 면 본인이 추가한 부분 조회
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            List<ProductResponseDto> list = new ArrayList<>();
            List<Product> productList;

            if (userRoleEnum == UserRoleEnum.USER) {
                // 사용자 권한이 USER일 경우
                productList = productRepository.findAllByUserId(user.getId());
            } else {
                productList = productRepository.findAll();
            }

            for (Product product : productList) {
                list.add(new ProductResponseDto(product));
            }

            return list;

        } else {
            return null;
        }
    }

    @Transactional
    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 최저가 업데이트 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            Product product = productRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
            );

            product.update(requestDto);

            return product.getId();

        } else {
            return null;
        }
    }

        @Transactional
        public void updateBySearch (Long id, ItemDto itemDto){
            Product product = productRepository.findById(id).orElseThrow(
                    () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
            );
            product.updateByItemDto(itemDto);
        }

    }
  • JWT UI로 확인해보기

profile
기록하는 습관

0개의 댓글