데브코스 백엔드 3기 71일차

목금!
JWT를 이용한 로그인 API와 인증된 사용자가 본인의 정보를 확인하는 API를 구현했다
마지막으로 카카오 로그인하기를 구현했다


일기(회고)

  • Spring Security 강의가 끝이 났고,
    기대가 되었던 kakao oauth도 해보았다
    전반적인 Spring Security 후기는 쉽지 않았다..!는 결론
    그렇지만 강의 안밀리고, 포기 안하고 끝까지 따라간 나 칭찬해


  • 객사오책을 다 읽었다!
    JPA 게시판 과제도 잘 제출했다
    이번주도 하루에 하나씩 테코톡 잘 보았고,
    CS 스터디도 잘했고(다음주에 벌써 끝..!)
    알고리즘도 잘풀었고(알고리즘은 얼른 잘하고싶다)
    나와의 약속을 잘 지켜서 너무 뿌듯하다!
    실력도 늘었나에 대한건 현재 단계에서 스스로 판단하긴 어렵다고 생각한다
    의심이 들 땐 내가 투자한 시간을 믿는중


  • 다음주는 데브코스 방학!(복습과 휴식)겸 새해!
    모처럼 힐링을 하려 가족과 칭9들과도 약속을 잡아보았다
    그 외의 시간에는 정리한 내용들 복습하기 & 알고리즘은 계속 하루에 하나씩 이어나가기
    계속해서 새로운걸 공부해 나가는것도 중요하지만
    배운걸 유지해야 아는게 늘어날테니
    정리기간을 가지려 한다!


  • 22년이 일주일 남았다
    싱숭생숭하다
    한 해가 지나가는게 아쉬워, 이번해가 아쉬움이 많았던 해인가?하고 생각해보면 그건 또 아니다
    열심히 살았어 이번해도 ㅋㄷㅋㄷ
    그래도 지나가는 시간들에 대한 아쉬움은 어쩔수 없나보다
    시간이 역시 젤 중요해! 시간은 한번 지나가면 돌아오지 않으니까!(청춘 드라마!)
    내년도 행복하자 태햐

    1222



JWT를 이용한 API 만들기

JWT를 이용한 로그인 API

(인증 API 추가하기)

  1. JWT 인증처리를 명확히 하기위해 JWT 전용 Authentication 구현체로 JwtAuthenticationToken 만들기
  2. 인증된 사용자의 Principal 타입으로 JwtAuthentication 만들기(userDetails.User 같은)
  3. 위의 2개의 클래스를 추가함으로써 Spring Security의 인증처리기능 일부 커스터마이징 필요


JwtAuthenticationFilter 만들기

  • 역할
    • HttpRequest의 Http Header에서 token 꺼내고
    • JWT 디코딩해서 JwtAuthenticationToken 만들어서
    • SecurityContext에 참조를 넘겨줌
  • 새로만든 필터를 SpringSecurityFilterChain에 몇번째에 위치시켜야할까 고민 -> SecurityContextPersistenceFilter 뒤에


JWT 전용 Authentication 구현체 만들기

  • JwtAuthenticationToken.java
  • extends AbstractAuthenticationToken
    • 인증 완료 전 호출되는 생성자
    • 인증 완료 시 호출되는 생성자
      • 이 생성자 안에서 setAuthenticated(true) -> 생성자 통해서만 true
public class JwtAuthenticationToken extends AbstractAuthenticationToken {

  private final Object principal;

  private String credentials;

  public JwtAuthenticationToken(String principal, String credentials) {
    super(null);
    super.setAuthenticated(false);

    this.principal = principal;
    this.credentials = credentials;
  }

  JwtAuthenticationToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    super.setAuthenticated(true);

    this.principal = principal;
    this.credentials = credentials;
  }


JWT로 인증된 사용의 Principal 타입 만들기

  • JwtAuthentication.java
  • token 과 username 가짐


UserDetailsService 역할 만들기

  • UserDetailsService : UserDetails(안에 User객체) 객체 가져와주는 애인데, 이젠 내가 만든 User 객체 사용
  • UserService.java 구현 : 로그인할 수 있게 UserRepository에서 find해서 User객체 찾음, 그 객체안에서 password 체크하는 로직 가짐


JWT인증 전용 AuthenticationProvider 구현체 만들기
JwtAuthenticationToken type의 인증요청을 처리할 수 있는 Provider

  • supports() : 주어진 Authentication 객체가 JwtAuthenticationToken으로 casting 가능한지 확인하게끔 override
  • authenticate() : 주어진 Authentication을 JwtAuthenticationToken으로 casting
  • UserService의 login(principal, credentials) 통해 로그인해서 User객체 얻어옴
    • Filter에서 User 정보로(사용자아이디, 권한목록으로) Claims 만들어서 jwt.sign(claims) 통해 token 만들어서 JwtAuthenticationToken 만듬
    • JwtAuthenticationToken authenticated = new JwtAuthenticationToken(new JwtAuthentication(token, user.getLoginId()), null, authorities);


Controller

  • LoginRequest(DTO) 이용해서
    • 인증요청 위한 JwtAuthenticationToken 만들어서
    • AuthenticationManager 통해 인증요청
    • 인증완료된 JwtAuthenticationToken에서 getPrincipal()에서 getDetails() 해서 내가만든 JwtAuthentication객체 얻음


Spring Security configuration class에 설정

  • JwtAuthenticationFilter 빈 등록 후 SecurityContextPersistenceFilter 뒤에 끼워넣어주기
    • .addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class)
  • JwtAuthenticationProvider 빈 등록
  • AuthenticationManager 빈 등록



JWT를 통해 인증된 사용자가 본인의 정보를 확인하는 API

  • @AuthenticationPrincipal : 인증된 사용자의 정보 가져오기 위해 사용
    • HttpRequest의 Header에 입력되어 있는 JWT 가져와서
    • 디코드 후 위변조 검증 후 제대로 들어있다면
    • SecurityContextLoader에서 Context 가져와서 Authentication 객체(구현체)에서 getPrincipal() 통해 Principal 객체(내가만든 JwtAuthentication)가 Controller에게 전달됨



  • 여기까지 커스텀하게 구현한 JwtAuthenticationFilter 사용한 방법
  • Spring Security에서 JwtAuthenticationFilter와 비슷한 처리하는 필터 : SecurityContextPersistenceFilter
    • SecurityRepository를 loadContext()통해서 SecurityContext 가져옴
    • SecurityRepository를 커스텀 구현하면 JWT 사용할때 가능
      • 그러나, SecurityRepository를 사용하는게 SessionManagementFilter도 있어 변경한다면 이부분 또한 설정 필요 -> 고려할게 많음
      • 따라서, JwtAuthenticationFilter 구현하기 추천



Spring Security OAuth2.0

사용자가 가입된 서비스(google, naver, kakao 등)에서 제공하는 API 이용해 사용자 데이터 접근할 때
비밀번호 없이 사용자로의 권한 위임받아야 함
=> OAuth2.0(Open Authorization, Open Authentication)이라는 표준 인증 프로토콜 통해 처리


  • Resource Owner : 리소스 주인, 서비스를 사용하는 사용자
  • Client : Resource Owner 대신해서 리소스에 접근하려는 응용 프로그램
  • Authorization Server : Client에 access token 발급하는 서버(naver, kakao 인증서버)
  • Resource Server : 리소스 호스팅하고 access token 받으면 응답하는 서버(naver, kakao 서버)



OAuth2.0에서 사용하는 토큰 요청법(Client -> Authorization Server)

1. Authorization Code Grant

  • 가장 잘 알고 있어야 함
  • Server to Server 인증방식 -> Back-end server 필요
  • 보안상 우수 : Client-secret, access token 같은 민감정보가 서버 밖으로 노출되지 않음
  • flow
    • 1)Authorization Request
      • application(Client) -> 사용자(Resource Owner) : Authorization Server로 redirection
      • response-type(“code” 고정값), client_id(클라이언트 고유 식별키), redirect-uri(인증 완료 후 백엔드 서버 호출시 사용할 uri), scope(Client가 요구하는 리소스), state(CSRF 공격방지 위한 임의의 문자열) 등을 가지고
    • 2)Authorization Response
      • Authorization Server -> application : 사용자가 로그인하고 나면 위에서 보낸 redirect-uri로 redirection
      • code(access token 교환을 위한 1회성 승인코드), state(위에서 전달한 임의의 문자열) 가지고
      • code는 URL에 노출됨 -> 한번 쓰면 바로 폐기함
    • 3)Token Request
      • application -> Authorization Server : 위에서 받은 code를 가지고 access token으로 교환요청
      • grant-type(“authorization” 고정값), code(위에서 받은 1회성 승인코드), client_id, client_secret(클라이언트 비밀키)을 가지고
    • 4)Token Response
      • Authorization Server -> application : access token, refresh token 응답


2. Implicit Grant

  • Authorization Code Grant에서 Token Request 단계 생략
    • Authorization Response 시에 code 대신 바로 access token 전달
      -> access token이 URL에 노출
      -> 보안상 리스크(백엔드 서버 없는 제한적인 환경에서만 사용권장)


3. Client Credentials Grant

  • 사용자(Resource Owner)는 전혀 관여x, 백그라운드에서 실행되는 서버간 통신에 적용할 수 있는
  • flow
    • application(Client) -> Authorization Server : client_id, client_secret 보냄
    • access token 받음


4. Resource Owner Password Credentials Grant

  • 일반 로그인 인증과 같음
  • 인증정보가 Client를 거쳐가므로 Client를 완전히 신뢰할 수 있을 때 사용
    • third party application에서는 잘 못쓰고, 공식 service application 정도 되어야 적용해 볼 수 있음
  • flow
    • 사용자(Resource Owner) -> application(Client) -> Authorization Server : 사용자의 (naver, kakao의) id, password가 application 거쳐서 인증서버까지
    • access token 받음



Kakao OAuth2.0 Login 구현하기

  • 카카오 로그인하기 연동을 위해 kakao developers site에 application 등록
    • kakao developers에서 application 추가
    • 로그인 활성화 on
    • redirect url 등록
    • 동의항목(닉네임, 프로필사진) 필수동의로
    • 보안 - Client Secret 생성 + 활성화
  • spring-boot-starter-oauth2-client dependency 추가
  • application.yml에 설정
    • registration
      • client-id : Rest API키
      • client-secret : 보안파트에서 만든 Client Secret
      • redirect-uri : kakao 부분 {registrationId}로 변수처리
    • provider : kakao Rest API 주소들
      • authorization-uri : 인가코드(1회성 인증코드) 받기 API path
      • token-uri : 1회성 인증코드를 이용해 access token 받기 API path
      • user-info-uri : access token으로 사용자 정보 가져오기 위한 API
      • user-name-attribute : 사용자 정보 가져오기 API에서 정보 가져왔을 때 사용자 고유 식별키 추출위한 고유 식별키 필드명
        • 사용자 정보 가져오기의 응답모델을 살펴보면 id가 회원번호라고 되어있음
          = id라는 필드가 카카오에서 사용자를 식별하기 위한 고유키라는 뜻
spring: 
  security:
      oauth2:
        client:
          registration:
            kakao:
              client-name: kakao
              client-id: 
              client-secret: 
              scope: profile_nickname, profile_image
              redirect-uri: "http://localhost:8080/login/oauth2/code/{registrationId}"
              authorization-grant-type: authorization_code
              client-authentication-method: POST
          provider:
            kakao:
              authorization-uri: https://kauth.kakao.com/oauth/authorize
              token-uri: https://kauth.kakao.com/oauth/token
              user-info-uri: https://kapi.kakao.com/v2/user/me
              user-name-attribute: id



인증완료 후 호출되는 handler 만들기

  • handler 빈등록
  • oauthlogin의 successHandler로 등록
  • 기능
    • 카카오 인증후의 사용자가 application의 신규 사용자라면 가입시키기(만들었던 UserService 수정해서 이용)
    • 카카오 인증후의 사용자에게 JWT를 생성해서 보내주기
  • users table 새로 만들기
    • password 없애고
    • provider : oauth 인증 provider의 식별자(kakao)
    • provider_id : oauth 인증된 사용자의 고유 식별키
    • profile_image : 사용자 프로필 이미지 url 저장
  • OAuth2.0으로 인증된 사용자 : OAuth2AuthenticationToken type
    • OAuth2AuthenticationToken에서 principal : OAuth2User
      • OAuth2User에서 getName() -> providerId(String)
      • OAuth2User에서 getAttributes() -> Map<String, Object> 형태로 attributes
        • attributes에서 get(“properties”) -> Map<String, Object> 형태로 properties
          • properties에서 get(“nickname”), get(“profile_image”) -> 사용자정보(닉네임, 프로필이미지) 얻음



내부적으로 일어나는 일

  • Spring Security의 Filter Chain에 추가된 필터들
    • OAuth관련 2개
      • OAuth2AuthorizationRequestRedirectFilter : redirection 시켜주는
      • OAuth2LoginAuthneticationFilter : 카카오에서 인증 후 백엔드 서버 호출(1회용 코드 줄때)시, 호출되는 필터
    • DefaultLoginPageGeneratingFilter : 인증방식 별로 적절한 id, pwd 기반의 폼로그인 페이지 생성해줌



쿠키기반인 AuthorizationRequestRepository의 커스텀 구현체 만들기

  • OAuth관련 필터 2개가 서로 연결되는 부분 있음 - CSRF 공격 방지 위한 임의의 문자열(state) 확인하는 부분
    • OAuth2AuthorizationRequestRedirectFilter에서 AuthorizationRequestRepository를 통해 authorizationRequest 저장
    • OAuth2LoginAuthneticationFilter에서 AuthorizationRequestRepository를 통해 authorizationRequest 조회
  • AuthorizationRequestRepository의 기본 구현체 : HttpSessionOAuth2AuthorizationRequestRepository -> session 기반
    => API 서버에 맞추어 cookie 기반으로 변경해보기(커스텀) : HttpCookieOAuth2AuthorizationRequestRepository 만들기
    • HttpServletRequest를 넘겨서 cookie 조회하는 메서드
    • cookie clear 하는 메서드
    • cookie로부터 OAuth2AuthorizationRequest 만드는 메서드(deserialize)
    • <-> Object를 cookie에 저장하는 메서드(serialize, save) : OAuth2AuthorizationRequest를 String으로 변환하고 그 String을 cookie에 저장



Spring Security OAuth2 JDBC방식으로 바꾸기

  • AuthorizedClientRepository에서 authorizedClient를 save함
    • AuthorizedClientRepository의 구현체 OAuth2AuthorizedClientRepository의 구현체 AuthenticatedPrincipalOauth2AuthorizedClientRepository에서 OAuth2AuthorizedClientService의 메서드 사용
    • OAuth2AuthorizedClientService의 기본 구현체 : InMemoryOAuth2AuthorizedClientService
    • JDBC기반 구현체로 변경해주기(제공되는) : JdbcOAuth2AuthorizedClientService
      • 얘가 사용하는 테이블도 이미 정의되어 있음(oauth2-client-schema.sql)



이번주 영상(테코톡 등)

12/19 월, 완태의 전략패턴

  • 전략패턴 : 디자인패턴 중 하나
    • 객체가 할수있는 행위를 동일계열의 알고리즘군으로 정의해서 캡슐화함으로써 전략으로 만들어 놓고 사용
    • setter 사용해서 상호교환을 통해 동적으로 전략수정이 가능
  • 예시) 로봇
    • 기능1 로봇/ 기능2 로봇/ 기능3 로봇.class 를 각각 만드는게 아니라
    • 기능1/ 기능2 / 기능3.class(strategy) 를 각각 만들고, 기능1, 2, 3을 가진(composition) 로봇.class(context)를 만들기
    • 기능1/ 기능2 / 기능3.class 를 구현하는 클래스들을 만들기
  • Java Comparator가 대표적 예시 : 유저들마다 커스텀하게 정의한 Comparator 구현해서 sort라는 컨텍스트에서 사용



12/20 화, 프로그래밍 초식 - DB 트랜잭션 조금 이해하기 01

  • 트랜잭션의 범위는 하나의 커넥션
    • 여러 메서드 호출 시 묶고 싶은데 그러려면 커넥션객체 전달해야 함
  • 커넥션객체 전달 안하고도 한 트랜잭션에 묶이도록 하기 위해서 트랜잭션 전파 필요
  • 외부 API 연동이 섞여있으면 롤백처리 주의
    • 외부 API는 록백이 안됨 -> 외부 시스템의 상태를 돌리는 방법 고려해야함



12/21 수, 프로그래밍 초식 - DB 트랜잭션 조금 이해하기 02

  • 경쟁상태 : 여러클라이언트가 같은 데이터에 접근하려고 할 때 문제 발생
    • -> 트랜잭션 순서대로 실행 : 한번에 한개 트랜잭션만 처리 -> 성능저하
    • -> 트랜잭션 격리
      • Read Uncommitted
      • Read Committed
      • Repeatable Read
      • Serializable


  • Read Committed : dirty read/write(커밋되지 않은 데이터 읽기, 변경하기) 문제 방지
    • 커밋된 데이터만 읽기
      • 커밋된 값(언두로그), 트랜잭션 진행중인 값(InnoDB 버퍼풀) 따로 보관
    • 커밋된 데이터만 변경하기
      • 행단위로 잠금 사용 -> 같은 데이터 수정하려고 하는 트랜잭션은 수정 중인 트랜잭션 끝날때까지 대기
  • Repeatable Read : read skew(읽는 시점에 따라 데이터가 바뀜) 문제 방지
    • 트랜잭션 동안 같은 데이터를 읽게함
    • ex) MVCC(Multi-Version Concurrency Control) : 읽을 때 읽는동안 다른 트랜잭션에 값이 변경 or 삭제되더라고 특정버전에 해당하는 데이터만 읽음
  • Lost update : 같은 데이터를 update 하다가 변경한 내용 유실될 수 있음
    • 해결
      • DB가 지원하는 원자적(atomic) 연산 사용
      • 명시적 잠금 : 수정위해 조회해오면서 수정할 행 미리 잠금(Select … for update)
      • CAS : 수정할 때 값이 같은지 비교 후 같을때만 수정
  • Serializable : 같은 데이터를 쓰지 않지만 트랜잭션의 결과가 다른 트랜잭션에 영향미쳐 논리적으로는 경쟁상태일 때(물리적으론 아닌데)를 방지
    • 인덱스 기반 잠금, where 절의 조건기반 잠금
    • 잠금시간은 최소화해야, 길어지면 성능(처리량)저하



12/22 목, 로키의 상속보다는 composition

  • 조합 : 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 설계
    • 장점
      • 캡슐화를 깨뜨리지 않음
      • 변화에 유연
      • 새로운 클래스는 기존클래스의 내부구현에 영향에서 벗어남(심지어 메서드 하나가 추가되어도 영향없음)
  • has a 관계



12/23 금, 조조그린의 Thread Pool

요청마다 thread 생성하면 안좋은점

  • Thread 생성비용 큼 -> 매번 만들기는 응답시간 늘어남
  • 요청 많아지면, 스레드 많아짐 -> 하드웨어쪽 무리, 오버헤드 때문에 프로그램 멈출 수도
    • => 메모리 차치 많음 -> 메모리 문제
    • => Context Switching 늘어남 -> CPU 오버헤드
  • Java : One-to-One Threading Model로 thread 생성
    • User thread(process의 thread) 생길 때 그에 해당하는 OS thread 하나 꼭 연결해줘야함
    • User thread : OS thread에 대한 유저프로그램 계층에서의 추상화
      => thread 하나 만들 때마다 OS kernel의 작업 필요
      => 생성비용 많이듬 -> 요청처리 시간 늘어남

=> 해결 : Thread Pool
Thread Pool : thread를 허용된 개수 안에서 사용 -> 동시요청을 안정적으로 처리

  • 자바에서의 Thread Pool : Thread Pool Executor
    • max pool size : 최대 스레드 수
    • core pool size : 최소 스레드 수
    • keep alive time : 이 시간 후에 요청 없으면 스레드 없어짐, 최소 core pool size만큼은 유지
  • Tomcat의 Thread Pool
    • max connections : Tomcat이 최대로 동시에 처리할 수 있는 커넥션(요청)의 개수
    • accept count : max connections이상의 요청이 들어왔을 때 사용하는 대기큐의 사이즈
    • max connections + accept count 이상의 요청이 들어오면 거절될 수 있음


업데이트: