0322 - 0328

0322

JWT (JSON Web Tokens)

JSON 객체를 사용해서 토큰 자체에 정보들을 저장하고 있는 Web Token

  • Signature를 해싱하기 위한 알고리즘 정보

Payload

  • 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보

Signature

  • 토큰의 유효성 검증을 위한 문자열
  • 이 문자열을 통해 서버에서는 이 토큰이 유효한 토큰인지 검증할 수 있다.

장점

  • 세션/쿠키 방식은 별도의 저장소의 관리가 필요하다. 그러나 JWT는 발급한 후 검증만 하면 되기 때문에 추가 저장소가 필요 없어서 stateless 한 서버를 만들수 있다.
  • 서버를 확장하거나 유지, 보수하는데 유리하다.
  • 토큰 기반으로 하는 다른 인증시스템에 접근이 가능하다. 예를들어 Google 로그인 등은 모두 토큰을 기반으로 인증한다.

단점

  • Payload의 정보가 많아지면 네트워크 사용량 증가, 데이터 설계 고려 필요
  • 토큰이 클라이언트에 저장되어 서버에서 클라이언트 토큰을 조작할 수 없음

JWT 인증방식

  1. 먼저 브라우저에서 Login요청을 한다.
  2. 서버에서는 계정정보를 읽어 사용자를 확인한 후, 사용자의 고유한 ID값을 부여한 후, 기타 정보와 함께 Payload에 넣는다.
  3. JWT 토큰의 유효기간을 설정한다.
  4. 암호화할 secret key를 이용해 ACCESS TOKEN을 발급한다.
  5. 사용자는 토큰을 받아 저장한 후, 인증이 필요한 요청마다 토큰을 헤더에 실어 보낸다.
  6. 서버에서는 해당 토큰의 verify signature를 secret key로 복호화한 후, 조작 여부, 유효기간을 확인한다.
  7. 검증이 완료된다면, Payload를 디코딩하여 사용자의 ID에 맞는 데이터를 가져온다.

간단 사용법

대부분의 복잡한 것들은 빌더 기반의 유용한 인터페이스 뒤에 숨겨져 있으며, 빠르게 코드를 작성하는 데 적합

Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
  1. 서브젝트가 세팅된 등록된 클레임을 가진 JWT를 빌딩한다.
  2. SHA-256 알고리즘으로 JWT를 서명한다.
  3. 문자형태로 컴팩팅한다. 서명된 JWT를 JWS라 부른다.

Signed JWTs

JWT 사양은 JWT에 암호화 방식으로 서명하는 기능을 제공한다.

  1. JWT가 우리가 아는 사람에 의해 생성되었음을 보장한다.
  2. JWT가 생성 된 후 아무도 JWT를 조작하거나 변경하지 않았음을 보장한다.

JWT 서명 과정

{
  "alg": "HS256"
}
String header = '{"alg":"HS256"}'
String claims = '{"sub":"Joe"}'

String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )

String concatenated = encodedHeader + '.' + encodedClaims

Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )

String jws = concatenated + '.' + base64URLEncode( signature )
  1. JSON header와 body(Claims)가 있는 JWT가 있다고 가정하면,
  2. JSON에서 불필요한 공백을 제거하고, UTF-8 바이트와 Base64URL 인코딩을 각각 가져온다.
  3. 인코딩 된 헤더와 클레임을 그들 사이에 마침표 문자로 연결한다.
  4. 선택한 서명 알고리즘과 함께 암호화 비밀 또는 개인 키를 사용하고 연결된 문자열에 서명한다.
  5. 서명은 항상 바이트 배열이기 때문에 Base64는 서명을 URL 인코딩하고 마침표 문자 ‘.’를 추가한다.
  • 이것을 JWS라고 하며, 서명된 JWT의 약자이다.
  • 물론 코드에서 수동으로 이 작업을 수행하지 않는다.

비밀 키 생성 - SHA-256 사용

  • 만약에, JWT HMAC-SHA 알고리즘을 사용하기 위해서 충분히 강력한 SecretKey를 생성하기 원한다면, Keys.secretKeyFor(SignatureAlgorithm) 메서드를 사용한다.
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512
  • 만약에 사용자가 이 새로운 SecretKey를 저장하길 원한다면, Base64 (혹은 BaSE64URL) 인코딩을 사용할 수 있다.
String secretString = Encoders.BASE64.encode(key.getEncoded());
  • Base64 인코딩은 암호화가 아니므로 여전히 민감한 정보로 간주되어 안전한 곳에 저장해야 한다.

0323

JWS 생성

String jws = Jwts.builder() // (1)
    .setSubject("Bob")      // (2)
    .signWith(key)          // (3)
    .compact();             // (4)
  1. Jwts.builder() 메서드를 사용하여 JwtBuilder 인스턴스를 생성한다.
  2. 원하는대로 헤더 매개 변수와 클레임을 추가하는 메서드를 호출한다.
  3. JWT에 서명하는 데 사용할 SecretKey를 지정한다.
  4. compact() 메서드를 호출하여 압축하고 서명하여 최종 JWS를 생성한다.

헤더 매개 변수

JWT 헤더는 JWT의 클레임과 관련된 콘텐츠, 형식 및 암호화 작업에 대한 메타 데이터를 제공한다.

Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
    .setHeader(header)
    // ... etc ...
  • 헤더를 설정하는 데는 헤더 인스턴스를 사용하는 방법과, 헤더 맵을 사용하는 방법이 있는데 나는 한 번에 처리할 수 있는 헤더 맵을 사용하여 설정하였다.

Claims

클레임은 JWT의 본문(페이로드)이며 JWT 작성자가 JWT 수신자에게 제공하기를 원하는 정보를 포함한다.

표준 클레임

  • setIssuer: 발행자 클레임을 설정한다.
  • setSubject: 주제를 설정한다.
  • setAudience: 받는 사람을 설정한다.
  • setExpiration: 만료 시간을 설정한다.
  • setNotBefore: 동작하지 않는 시간을 설정한다.
  • setIssuedAt: 발급 시간을 설정한다.
  • setId: 아이디를 설정한다.
Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
    .setClaims(claims)
    // ... etc ...

Signing Key

  • JwtBuilder의 signWith메서드 를 호출하여 서명 키를 지정하고 JJWT가 지정된 키에 허용되는 가장 안전한 알고리즘을 결정 하도록하는 것이 좋다.
String jws = Jwts.builder()
   // ... etc ...
   .signWith(key) // <---
   .compact();
  • signWith JJWT를 사용할 때 alg관련 알고리즘 식별자와 함께 필요한 헤더도 자동으로 설정됩니다 .

SecretKey 형식

  • 만약에 사용자가 HMAC-SHA 알고리즘으로 JWS를 서명하기 원하고, 사용자가 String 형식 혹은 인코딩 된 byte 배열 형태의 비밀 키를 가지고 있다면, 사용자는 그것을 signWith 메서드 매개변수로서 사용하기 위해 ScreteKey 객체로 변환해야 한다.

Reading a JWS

  1. JwtParserBuilder 객체를 생성하기 위해서 Jwts.parseBuilder() 메서드를 사용한다.
  2. JWS 서명을 증명하기 위해 사용하고 싶은 SecretKey 혹은 비대칭 PubliKey를 명세한다.
  3. 쓰레드에 안전한 JwtParser를 리턴하기 위해 JwtParserBuilder에 build() 메서드를 호출한다.
  4. parseClaimsJws(String) 메서드를 원본 JWS를 만드는 jws String와 함께 호출한다.
  5. 파싱이나 서명 유효성이 실패하는 경우에 try/catch 블록 안에서 모든 호출이 래핑된다.
Jws<Claims> jws;

try {
    jws = Jwts.parserBuilder()  // (1)
    .setSigningKey(key)         // (2)
    .build()                    // (3)
    .parseClaimsJws(jwsString); // (4)

    // we can safely trust the JWT

catch (JwtException ex) {       // (5)

    // we *cannot* use the JWT as intended by its creator
}
  • 주의: 만약 JWS를 기대한다면, 항상 JwtParser의 parseClaimsJws 메서드를 호출하여라.

검증 키

  • JWS를 읽을 때 가장 중요한 것은 JWS의 암호화된 signature을 증명하기 위해서 키를 명세하는 것.
  • signature 증명이 실패하면, JWT는 안전하게 신뢰될 수 없으며 폐기 되어야만 한다.
  • 만약 jws가 SecretKey로 서명되었다면, 같은 SecretKey는 JwtParserBuilder에 명세되어야 한다.

0324

쿠키

동작방식

  1. 쿠키 생성 단계
    • 생성한 쿠키를 응답 데이터의 헤더에 저장하여 웹브라우저에 전송
    • 사용자가 request 보내면, 서버가 response하는 시점에 setCookieHeader 라는걸 통해서 response
    • 브라우저가 응답 헤더를 보고 쿠키를 만든다.
  2. 쿠키 저장 단계
    • 웹 브라우저는 응답데이터에 포함된 쿠키를 쿠키 저장소에 보관
  3. 쿠키 전송 단계
    • 웹 브라우저는 저장한 쿠키를 요청이 있을 때마다 웹 서버에 전송
    • 사용자가 웹서버에 요청할 때마다, 요청한 url에 맞는 저장된 유효한 쿠키가 있는지부터 확인해서 쿠키를 요청시에 함께 보냄
    • 웹 서버는 브라우저가 전송한 쿠키를 사용하여 필요한 작업을 수행

쿠키 설정

쿠키 객체 생성

Cookie cookie = new Cookie("key값", "value값");

  • 한글을 사용할 시에는 URLEncoder.encode()를 사용하여 인코딩 처리를 해준다.

쿠키 유효 시간 설정

cookie.setMaxAge(60 * 60 * 24);

  • 초 단위로 지정한다.
  • 지정하지 않으면 브라우저를 종료할 때 쿠키를 함께 삭제한다.

패스

cookie.setPath("/");

  • 쿠키는 쿠키 데이터를 생성한 웹 페이지에서만 데이터를 읽을 수 있지만, Path를 지정해주면 그 이하의 경로에서 쿠키 데이터를 공유할 수 있다.

도메인

cookie.setDomain("");

  • 패스가 하나의 사이트에서 쿠키 데이터를 읽고 쓰는 권한을 설정하는 것이라면, 도메인 항목은 도메인 단위에서 쿠키 데이터를 읽고 쓰는 권한을 설정하게 된다.

보안

cookie.setHttpOnly(true);

  • HttpOnly는 자바스크립트의 document.cookie를 이용 해 쿠키에 접속하는 것을 막는 옵션이다. cookie.setSecure(true)
  • 웹 브라우저와 웹 서버가 HTTPS로 통신하는 경우에만 웹 브라우저가 쿠키를 서버로 전송하는 옵션이다.

응답 헤더에 쿠키 객체를 추가

response.addCookie(cookie);

쿠키 조회

Cookie[] cookies = request.getCookies();

if(cookies != null) {
	for(Cookie cookie: cookies) {
		System.out.println("쿠키 명: " + cookie.getName());
		System.out.println("쿠키 값: " + cookie.getValue());
	}
}

쿠키 삭제

Cookie[] cookies = request.getCookies();

if(cookies != null) {
	for(Cookie cookie: cookies) {
		cookie.setMaxAge(0); 		  // 유효시간을 0으로 설정
		response.addCookie(cookie); // 응답 헤더에 추가
	}
}

0325

Mabatis

SelectKey

INSERT 수행 이전, 이후에 생성하거나 알 수 있는 값이 필요한 경우 사용

  • DB에 명령을 한 번만 보내어, 입력한 값으 결과값을 다음 쿼리로 바로 return 시켜준다.

속성

  • keyProperty
    -> selectKey 구문의 결과가 세팅될 대상 프로퍼티
  • keyColumn
    -> 리턴되는 결과셋의 칼럼명은 프로퍼티에 일치한다. 여러 개의 컬럼을 사용한다면 컬럼명의 목록은 콤마를 사용해서 구분
  • resultType
    -> 결과의 타입. String을 포함하여 키로 사용될 수 있는 간단한 타입을 허용한다.
  • order
    -> BEFORE 또는 AFTER를 세팅할 수 있다. _ BEFORE로 설정하면 키를 먼저 조회하고 그 값을 keyProperty에 세팅한 뒤 insert 구문을 실행 _ AFTER로 설정하면 insert 구문을 실행한 뒤 selectKey 구문을 실행한다.

Insert 후 ID 받아오기

  • LAST_INSERT_ID()를 사용하면 된다.
<!-- 게시물 작성 -->
<insert id="save" parameterType="Board">
    INSERT INTO BOARD
    (
        MEMBER_NO,
        TITLE,
        CONTENT,
        REG_DATE
    )
    VALUES
    (
        #{memberNo},
        #{title},
        #{content},
        NOW()
    )
    <selectKey keyProperty="boardNo" resultType="long" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
</insert>
  • 이 때, 리턴값은 parameterType에 넘겨준 객체에 넘어간다.
  • 즉, 위에서는 이 메소드를 호출한 클래스에서 파라미터로 넘긴 board에서 바로 board.getBoardNo()로 해당값을 가져올 수 있다.

0326

게시글 생성 API 작성

컨트롤러 - 서비스 - 매퍼

// Controller
@RequiredArgsConstructor
@RestController
@RequestMapping("/boards")
public class BoardController {

    private final BoardService boardService;

    /**
     * 게시글 생성
     * @param board
     * @param request
     * @return
     */
    @PostMapping("/new")
    public BaseResponse insertBoard(@RequestBody Board board, HttpServletRequest request) {
        // 글제목 필수 체크
        if (isEmpty(board.getTitle())) {
            throw new BaseException(BaseResponseCode.CODE_100, new String[]{"글제목"});
        }
        // 글내용 필수 체크
        if (isEmpty(board.getContent())) {
            throw new BaseException(BaseResponseCode.CODE_100, new String[]{"글내용"});
        }

        // request에 저장한 id를 받아와서 Board 객체에 값을 넣어준다.
        long memberNo = getMemberNo(request);
        board.setMemberNo(memberNo);

        long boardNo = boardService.insertBoard(board);
        Map<String, Object> ret = new HashMap();
        ret.put("boardNo", boardNo);

        return new BaseResponse(ret);
    }
}

// Service
public interface BoardService {

    long insertBoard(Board board);
}

// ServiceImpl
@Service
@Transactional
@RequiredArgsConstructor
public class BoardServiceImpl implements BoardService {

    private final BoardMapper boardMapper;

    @Override
    public long insertBoard(Board board) {
        boardMapper.save(board);
        return board.getBoardNo();
    }
}

// Mapper
@Repository
@Mapper
public interface BoardMapper {

    long save(Board board);
}
  • 제목과 내용은 필수로 입력받도록 해서 없을 경우 에러 메시지를 반환하도록 처리하였다.
  • jwt 토큰 처리를 해서 request에 사용자 id를 저장해두고 받아와서 새 게시글 db에 사용자 정보가 삽입될 수 있도록 했다.
  • 반환 값으로는 게시글 번호를 반환하여 reponse로 보내주었다.

mybatis query

<!-- 게시글 작성 -->
<insert id="save" parameterType="Board">
    INSERT INTO BOARD
    (
        MEMBER_NO,
        TITLE,
        CONTENT,
        REG_DATE
    )
    VALUES
    (
        #{memberNo},
        #{title},
        #{content},
        NOW()
    )
    <selectKey keyProperty="boardNo" resultType="long" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
</insert>
  • <selectKey>를 사용하여 반환 값으로 pk 값을 반환해주도록 하였다.

0327

게시글 조회 API 작성

게시글을 클릭했을 때 정보들을 프론트 단에서 사용하기 위해 response 값으로 반환해주기 위한 상세 조회 api를 작성하였다.

컨트롤러 - 서비스 - 매퍼

// Controller
/**
* 게시글 세부 조회
* @param boardNo
* @return
*/
@GetMapping("/{boardNo}")
public BaseResponse getBoard(@PathVariable("boardNo") long boardNo) {

    Board info = boardService.getBoard(boardNo);
    Map<String, Object> ret = new HashMap<>();
    ret.put("info", info);

    return new BaseResponse(ret);
}
  • path에 게시글 번호를 지정하여 그에 맞는 정보를 반환하도록 url을 설계하였다.
  • boardService.getBoard(boardNo)는 boardNo로 조회하여 그에 대한 게시물 정보를 반환한다.

mybatis query

<!-- 게시글 조회 -->
<select id="getBoard" parameterType="long" resultType="Board">
    SELECT
        B.BOARD_NO,
        A.MEMBER_NO,
        A.MEMBER_NAME,
        B.TITLE,
        B.CONTENT,
        B.COMMENT_CNT,
        B.LIKE_CNT,
        B.REG_DATE
    FROM MEMBER A INNER JOIN BOARD B ON A.MEMBER_NO = B.MEMBER_NO
    WHERE BOARD_NO = #{boardNo}
</select>
  • BOARD 테이블에 없는 MEMBER_NAME 컬럼을 출력하기 위해 MEMBER 테이블과 BOARD 테이블을 INNER JOIN하였다.
  • 예를 들어, http://localhost:8080/boards/3으로 호출하게 되면 아래와 같은 결과 값을 반환받게 된다.
{
  "code": "CODE_200",
  "message": "정상 처리",
  "data": {
    "info": {
      "boardNo": 3,
      "memberNo": 47,
      "memberName": "홍길동",
      "title": "제목테스트수정",
      "content": "내용테스트수정",
      "commentCnt": 0,
      "likeCnt": 0,
      "regDate": "2021-03-28 14:25:01"
    }
  }
}

0328

게시글 수정, 삭제 API 작성

게시글 수정 API

/**
* 게시글 수정
* @param boardNo
* @param board
* @param request
* @return
*/
@PostMapping("/{boardNo}/edit")
public BaseResponse updateBoard(@PathVariable("boardNo") long boardNo, @RequestBody Board board, HttpServletRequest request) {

    // 글제목 필수 체크
    if (isEmpty(board.getTitle())) {
        throw new BaseException(BaseResponseCode.CODE_100, new String[]{"글제목"});
    }
    // 글내용 필수 체크
    if (isEmpty(board.getContent())) {
        throw new BaseException(BaseResponseCode.CODE_100, new String[]{"글내용"});
    }

    // 현재 로그인한 회원 번호와 게시글 작성한 회원 번호를 비교해서 다른 경우에 예외 발생
    Board info = boardService.getBoard(boardNo);
    long memberNo = getMemberNo(request);
    if (memberNo != info.getMemberNo()) {
        throw new BaseException(BaseResponseCode.CODE_103);
    }

    board.setBoardNo(boardNo);
    boardService.updateBoard(board);

    return new BaseResponse();
}

게시글 삭제 API

/**
* 게시글 삭제
* @param boardNo
* @param request
* @return
*/
@PostMapping("/{boardNo}/delete")
public BaseResponse deleteBoard(@PathVariable("boardNo") long boardNo, HttpServletRequest request) {

    // 현재 로그인한 회원 번호와 게시글 작성한 회원 번호를 비교해서 다른 경우에 예외 발생
    Board info = boardService.getBoard(boardNo);
    long memberNo = getMemberNo(request);
    if (memberNo != info.getMemberNo()) {
        throw new BaseException(BaseResponseCode.CODE_103);
    }

    boardService.deleteBoard(boardNo);

    return new BaseResponse();
}

mybatis query

<!-- 게시글 수정 -->
<update id="updateBoard" parameterType="Board">
    UPDATE BOARD
    SET
        TITLE = #{title},
        CONTENT = #{content}
    WHERE BOARD_NO = #{boardNo}
</update>

<!-- 게시글 삭제 -->
<delete id="deleteBoard" parameterType="long">
    DELETE FROM BOARD
    WHERE BOARD_NO = #{boardNo}
</delete>
  • 게시글을 수정하고 삭제할 수 있는 API를 작성하였다.
  • 현재 로그인한 회원 정보와 요청 받은 게시물을 작성한 회원 정보를 비교해서 다른 경우에는 예외를 발생시켰다.

태그:

카테고리:

업데이트: