Spring 숙련 05-07

5w31892p·2022년 12월 14일
0

Spring

목록 보기
6/30

📜 Spring

:: 인증 / 인가

:: 인증

  • 실제 유저인지 인증
  • 지문인식, face id, 로그인 등

웹 어플리케이션의 인증

  • 서버-클라이언트 구조 : 둘이 사이가 멂
  • Http 라는 프로토콜을 이용하여 통신
  • 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어짐
비연결성(Connectionless)
- 서버와 클라이언트가 연결되어 있지 않다는 것
- 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버리고있다 라고 생각

무상태(Stateless)
- 서버가 클라이언트의 상태를 저장하지 않는다는 것

웹 어플리케이션의 인증처리 방법

  • 쿠키-세션 방식의 인증
    • 서버가 특정 유저가 로그인 되었다는 상태를 저장하는 방식
    • 인증과 관련된 최소한의 정보는 저장해서 로그인 유지

  1. 사용자 로그인 요청 보냄
  2. 서버는 사용자 확인 (DB 내 UserTable에서 id, password 대조)
  3. 정보 일치한다면 인증 통과로 보고 session storage에 해당 user 로그인 정보 넣음
  4. session storage에서는 session-id 발급
  5. 서버는 사용자의 요청을 session-id로 응답
  6. 사용자는 session-id를 쿠키에 보관하고 요청마다 session-id 같이 보냄 (HTTP header에 담아 보냄)
  7. 서버는 session storage에서 쿠키검증
  8. user 정보 받아왔다면 해당 사용자는 로그인되어 있는 사용자
  9. 로그인된 user에게 응답

  • JWT(JSON Web Token) 기반 인증
    • JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별
    • JWT : 인증에 필요한 정보들을 암호화시킨 토큰

  1. 사용자 로그인 요청
  2. 사용자 확인
  3. 유저의 정보를 JWT로 암호화해서 내보냄
  4. 로그인 요청을 jwt 토큰으로 응답
  5. 사용자는 토큰 저장소에 보관 후 요청마다 토큰 같이 보냄
  6. 서버 - 토큰 검증
  7. 로그인된 user에게 응답

redirect

  • 다시 지시하는 것
    • redirect:/api/user/login == /api/user/login 주소로 가라
    • redirect:/api/shop == /api/shop 주소로 가라

Optional <'T'>

  • Integer or Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스(Wrapper class)
  • 모든 타입의 참조 변수 저장 가능
  • 복잡한 조건문 없이도 널(null) 값으로 인해 발생하는 예외 처리 가능

:: 인가(Authorization)

  • 특정 리소스에 접근이 가능한지 허가 확인
  • 관리자 페이지, 관리자 권한

:: 쿠키와 세션

  • HTTP는 사용자 구별을 못함
  • 쿠키와 세션은 HTTP 에 상태 정보를 유지(Stateful)하기 위해 사용
  • 쿠키와 세션을 통해 서버에서는 클라이언트 별로 인증 및 인가

:: 쿠키

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일

구성요소

  • Name
    • 이름
    • 쿠기 구별하는데 사용되는 키
  • Value
    • 쿠키 값
  • Domain
    • 쿠기 저장된 도메인
  • Path
    • 쿠기 사용되는 경로
  • Expires
    • 쿠기 만료기한 (기한 지나면 삭제)

:: 세션

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 각 클라이언트에게 세션ID 부여 후 클라이언트 별 필요 정보 서버에 저장
  • 세션 ID 는 클라이언트의 쿠키값으로 저장
  • 서버는 세션ID 를 사용하여 세션을 유지
  • 같은 클라이언트의 첫번째 두번째 요청이 있을 때 각 요청이 같은 클라이언트임을 인지함

세션 동작 방식

  1. ct 서버에 1번 요청
  2. 서버가 세션 ID 생성 후 응답 헤더에 전달
  3. ct 쿠키 저장
  4. 서버에 2번 요청 (1번 요청 후 받은 세션 ID 포함하여 요청)

-> 서버가 세션 ID 확인하고, 1번 요청과 같은 ct 인 것을 인지

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

:: JWT (Json Web Token)

  • Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
  • 저장된 쿠키
  • 서버에서 발급받은 JWT 토큰을 Request에 담아 사용자의 정보 열람, 수정 등의 작업 수행 가능

서버 1대인 경우

  • Session1 이 모든 Client 의 로그인 정보 소유

서버 2대 이상인 경우

  • Session마다 다른 ct 로그인 정보 가지고 있는데, 해당 로그인 정보를 가지고 있지 않은 Server에 요청하면 문제가 생김

그러므로,
1. Sticky Session: Client 마다 요청 Server 고정
2. 세션 저장소 생성 -> Session storage 가 모든 Client 의 로그인 정보 소유

:: JWT 사용

  • 로그인 정보를 Server 에 저장하지 않고, Ct에 로그인정보를 JWT 로 암호화하여 저장
  • 그 후 JWT 통해 인증/인가
  • 모든 서버에서 동일한 Secret Key 소유
  • Secret Key 통한 암호화 / 위조 검증 (복호화 시)

복호화 
decoding
encoding된 정보를 code화 되기 전으로 되돌리는 처리
즉, 부호화된 정보를 부호화되기 전으로 되돌리는 처리

:: JWT 장점 / 단점

장점

  • 동시 접속자가 많을 때 서버 측 부하 낮춤
  • Client, Sever 가 다른 도메인을 사용할 때
  • (예) 카카오 OAuth2 로그인 시 JWT Token 사용

단점

  • 구현 복잡도 ↑
  • JWT에 담는 내용 커질수록 네트워크 비용 증가 (ct -> Server)
  • 생성된 JWT 를 일부만 만료 안됨
  • Secret key 유출 시 JWT 조작 가능

:: JWT 사용 흐름

ct 로그인 성공 시

  1. 로그인 정보 JWT로 암호화 (Secret Key 사용)

  2. JWT를 ct에 전달(전달 방법은 개발자가 정함)
    -> 전달 형태

    Authorization: BEARER <JWT>
    
    //ex)
    Authorization: BEARER blah blah ...
  3. ct에서 JWT 저장 (쿠키, Local storage 등)

ct에서 JWT 통해 인증방법

  1. JWT 를 API 요청 시마다 Header 에 포함
  2. Server
    -> Secret Key 사용하여 ct로부터 전달 받은 JWT 위조 여부 검증
    -> JWT 유효기간 검증
    -> 성공시 JWT에서 사용자 정보 가져와 확인
XSS(Cross Site Scripting)
- 악의적인 js 코드를 피해자 웹 브라우저에서 실행 
  -> 정보 탈취

CSRF
- 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행
  -> 정상적인 Request를 가로채 본인인 척 백엔드 서버에 변조된 Request를 보내 악의적인 동작을 수행하는 공격 (피해자 정보 수정, 정보 열람) 
  -> 내가 쓰지 않은 광고성 글이 SNS에 올라갈 수 있음
  • XSS 공격으로부터 Local storag에 비해 안전 (완전한 안전은 아님)
  • CSRF 공격에 취약
  • 용량 4KB

Local storag

  • CSRF 공격에 안전
  • XSS에 취약
  • 용량 5MB
  • 브라우저에 반영구적으로 저장
  • 브라우저를 종료해도 데이터가 유지되며 도메인 기준으로 저장
  • A.com에서 B.com에 저장한 데이터 접근 못함
  • localStorage에 데이터를 저장하고 가져오는 방법
localStorage.setItem('myCat', 'Tom');
var myCat = localStorage.getItem('myCat');

Seeeion storage

  • XSS에 취약
  • 용량 5MB
  • 각 세션마다 데이터가 개별적으로 저장
  • 즉 여러 개의 탭을 실행 중이면 각 탭 별로 따로 데이터가 저장
  • 세션이 다르면 다른 세션의 sessionStorage에 접근 못함
  • sessionStorage에 데이터를 저장하고 가져오는 방법
sessionStorage.setItem('myCat', 'Tom');
var myCat = sessionStorage.getItem('myCat');

로컬 스토리지와 세션 스토리지 차이

  • 로컬 스토리지는 해당 도메인에 설정된 정보값을 브라우저 탭, 창 간에 공유하지만,
  • 세션 스토리지는 공유하지 않음

:: JWT 구조

JWT Decode하는 사이트

  • Secret Key 가 없으면 JWT 수정 불가능
  • 누구나 평문으로 복호화 가능
  • JWT 는 Read only 데이터
    • readOnly = true -> 읽기전용
평문(Plaintext, Cleartext)

- 암호화되지 않은 정보
- 누구나 읽을 수 있는 문서

Transaction

  • 하나의 flow로 처리해야하는 로직
  • 데이터베이스의 상태를 바꾸기 위해 더 이상 쪼개질 수 없는 최소의 연산
  • ACID의 특성을 가짐

ACID

  • 원자성(Atomicity)
    • 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 특성
  • 일관성(Consistency)
    • 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 특성
  • 독립성(Isolation)
    • 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 특성
  • 지속성(Durability)
    • 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미하는 특징
    • 스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 함을 의미

@Transactional

  • Transaction이 되도록 보장해조는 어노테이션
상품을 주문하면 상점에서는 주문 접수가 되고, 라이더도 배정되어야 함
근데 상품 주문에는 문제없지만, 상점에서 접수될 때 문제가 생긴다면 주문된 상품은 어떻게 해야할까?

이런 문제에서 @Transactional을 사용하면 해당 메서드를 실행하기 전의 db 상태로 되돌릴 수 있음

@Transactional(readOnly = true)

  • (readOnly = true) 옵션을 사용하여 해당 메서드가 읽기 전용이라는 것을 명시
  • 영속성 컨텍스트에 관리를 받지않음
  • 변경감지 수행등을 하지 않아 @Transactional의 격리 수준보다 낮은 수준의 격리 수준을 사용

@Transactional(readOnly = true) 사용시 장단점

장점

  • readOnly라고 명시하므로 읽는 개발자가 읽기 전용 메서드라는 것을 알 수 있음
  • JPA를 사용할 경우, 변경감지 작업을 수행하지 않아 성능상의 이점
  • 트랜잭션 ID 설정에 대한 오버헤드를 해결
  • 스냅샷을 통해 데이터의 일관성을 보장
  • CRUD를 하는 service에서 클래스에 @Transactional을 붙여 중복을 줄이고 싶을 때 read 메서드에 사용하여 격리 수준을 바꿔줄 수 있음

단점

  • 사용하지 않았을 때보다 자원(프록시)을 사용하므로 느림

:: JWT 구현

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'

application.properties에 추가

// 임의로 만든 키임

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

토큰 생성에 필요한 값

// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
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에서 Token 가져오기

// 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;
}

JWT 생성

// 토큰 생성
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();
}

JWT 검증

// 토큰 검증
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;
}

JWT에서 사용자 정보 가져오기

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

@PostConstruct

  • 객체 생성 후 초기화 하는 메서드
  • 의존성 주입이 이루어진 후 초기화를 수행하는 메서드

JSON.stringify( )

  • 자바스크립트의 값을 JSON 문자열로 변환

:: JPA

:: Paging

  • Page화 하는 것
  • 조회 결과가 매우 많은 경우 일정한 크기를 갖는 페이지로 조회 목록을 쪼개는 것
  • 페이지 조건 줄 때 주로 Size / Page / Sort 3가지 값 필요로 함
  1. Size
    -> Page의 개수
    -> 100개의 목록이 있을 때 size가 9라면 12개의 페이지가 생성되고 마지막 페이지에는 1개의 목록만

  2. Page
    -> 생성된 페이지를 접근할 때 사용하는 인덱스

  3. Sort
    -> 페이지 자르기 전 먼저 목록 정렬 후 자를 수 있는데 이때 정렬할 기준 값 설정
    -> Sort.By("기준 컬럼 프로퍼티명").desending() 또는 .asending()으로 오름차순 내림차순 정함
    -> 기준 값으로 하고자하는 값은 객체에 존재하는 컬럼의 프로퍼티명

Spring Web MVC + 페이징 예시

@GetMapping("/all")
@ApiOperation(value = "채팅방 메세지 조회", notes = "페이징 방식으로 채팅방의 메시지를 최근 순서로 조회합니다.")
public ListResult<ChatMessageRes> getAllChats(
        @ApiParam(value = "채팅방 SEQ") @RequestParam Long chatRoomSeq,
        Pageable pageable
) {
    try {
        return responseService.getListResult(
                chatService.findAllChats(chatRoomSeq, pageable).getContent()
        );
    } catch (Exception e) {
        e.printStackTrace();
        throw new BussinessException(e.getMessage());
    }
}

:: 페이징 및 정렬 설계

Client → Server

  1. 페이징
    • page : 조회할 페이지 번호 (1부터)
    • size : 한 페이지에 보여줄 상품 개수 (10개)
  2. 정렬
    • sortBy(정렬항목)
      • id : Product 테이블의 id
      • title : 상품명
      • lprice : 최저가
    • isAsc
      • true: 오름차순 (asc)
      • false : 내림차순 (desc)

Server → Client

  • number: 조회된 페이지 번호 (0부터 시작)
  • content: 조회된 상품 정보 (배열)
  • size: 한 페이지에 보여줄 상품 개수
  • numberOfElements: 실제 조회된 상품 개수
  • totalElements: 전체 상품 개수 (회원이 등록한 모든 상품의 개수)
  • totalPages: 전체 페이지 수
    totalPages = totalElement / size 결과를 소수점 올림
            1 / 10 = 0.1 =>1 페이지
            9 / 10 = 0.9 =>1페이지
            10 / 10 = 1 =>1페이지
            11 / 10 => 1.1 =>2페이지
  • first: 첫 페이지인지? (boolean)
  • last: 마지막 페이지인지? (boolean)

페이징 구현체

/*
 * Copyright 2008-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.domain;

        import java.util.List;
        import java.util.function.Function;

        import org.springframework.lang.Nullable;

/**
 * Basic {@code Page} implementation.
 *
 * @param <T> the type of which the page consists.
 * @author Oliver Gierke
 * @author Mark Paluch
 */
public class PageImpl<T> extends Chunk<T> implements Page<T> {

    private static final long serialVersionUID = 867755909294344406L;

    private final long total;

    /**
     * Constructor of {@code PageImpl}.
     *
     * @param content the content of this page, must not be {@literal null}.
     * @param pageable the paging information, must not be {@literal null}.
     * @param total the total amount of items available. The total might be adapted considering the length of the content
     *          given, if it is going to be the content of the last page. This is in place to mitigate inconsistencies.
     */
    public PageImpl(List<T> content, Pageable pageable, long total) {

        super(content, pageable);

        this.total = pageable.toOptional().filter(it -> !content.isEmpty())//
                .filter(it -> it.getOffset() + it.getPageSize() > total)//
                .map(it -> it.getOffset() + content.size())//
                .orElse(total);
    }

    /**
     * Creates a new {@link PageImpl} with the given content. This will result in the created {@link Page} being identical
     * to the entire {@link List}.
     *
     * @param content must not be {@literal null}.
     */
    public PageImpl(List<T> content) {
        this(content, Pageable.unpaged(), null == content ? 0 : content.size());
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Page#getTotalPages()
     */
    @Override
    public int getTotalPages() {
        return getSize() == 0 ? 1 : (int) Math.ceil((double) total / (double) getSize());
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Page#getTotalElements()
     */
    @Override
    public long getTotalElements() {
        return total;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Slice#hasNext()
     */
    @Override
    public boolean hasNext() {
        return getNumber() + 1 < getTotalPages();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Slice#isLast()
     */
    @Override
    public boolean isLast() {
        return !hasNext();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Slice#transform(org.springframework.core.convert.converter.Converter)
     */
    @Override
    public <U> Page<U> map(Function<? super T, ? extends U> converter) {
        return new PageImpl<>(getConvertedContent(converter), getPageable(), total);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {

        String contentType = "UNKNOWN";
        List<T> content = getContent();

        if (!content.isEmpty() && content.get(0) != null) {
            contentType = content.get(0).getClass().getName();
        }

        return String.format("Page %s of %d containing %s instances", getNumber() + 1, getTotalPages(), contentType);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(@Nullable Object obj) {

        if (this == obj) {
            return true;
        }

        if (!(obj instanceof PageImpl<?>)) {
            return false;
        }

        PageImpl<?> that = (PageImpl<?>) obj;

        return this.total == that.total && super.equals(obj);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {

        int result = 17;

        result += 31 * (int) (total ^ total >>> 32);
        result += 31 * super.hashCode();

        return result;
    }
}

  • user 입장에서 folder == OneToMany
  • folder 입장에서 user == ManyToOne

@RequestParam(전달인자 이름(실제 값을 표시))

  • 단일 파라미터 변환
  • url 뒤에 붙는 파라미터 값 가져올 때 사용

@PathVariable (Long id)

  • url에서 각 구분자에 들어오는 값을 처리해야 할 때 사용

SQL WHERE절에서의 IN

  • 조건 범위 지정
  • 값은 콤마( , )로 구분
  • 값 중에서 하나 이상과 일치하면 조건에 맞는 것

오류 검사는 빠를수록 좋음

Service보다는 Controller에서 입력값 체크 후 뒤에 실행하는 것이 좋음

0개의 댓글