주노 님의 블로그

20240819 본캠프 26일차 TIL 본문

TIL

20240819 본캠프 26일차 TIL

juno0432 2024. 8. 19. 22:48

본캠프 26일차 내용 간단요약

  • 09:00 ~ 10:00 : 코드카타
  • 10:00 ~ 10:30 : 발제
  • 10:30 ~ 12:00 : erd 설계
  • 12:00 ~ 13:00 : 점심시간
  • 13:00 ~ 14:00 : api 설계
  • 14:00 ~ 18:00 : 강의
    심화 1주차 1강 ~ 1주차 10강
  • 18:00 ~ 19:00 : 저녁시간
  • 19:00 ~ 21:00 : 강의
    1주차 11강 ~ 1주차 17강

오늘 해야할 일 ✔️ 🔺 ❌

🔺 필수 요구사항 리팩토링

🔺 추가 요구사항 1-7까지 구현해보기!

도전할 기회가 또 생겨버린거잖앙?

 



 

강의 - 심화 1주차

더보기

bean 수동으로 등록해보기

  • 왜 수동으로 등록해야할까?
    같은 부류의 bean을 제어 가능 : 특정한 설정이나 제어가 필요한경우 묶을수 있음.
    공통적인 관심사를 처리할때 (기술 지원 bean) : 로깅 보안등 공통된 관심사를 처리
    문제가 발생했을때 문제 추적 용이때문에

    그외에는 자동으로 등록하는게 좋고, 편하기도 하다.

  • 수동으로 bean을 정의하는법
    프로젝트 최상단 부분에  config 파일을 만든다
    @Configuration
    public class PasswordConfig {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    그리고 Configuration어노테이션 을 정의해준다
    configuration으로 정의하면 spring이 시작될때 자동으로 @bean을 정의한다.

    @SpringBootTest
    public class PasswordEncoderTest {
    
        @Autowired
        PasswordEncoder passwordEncoder;
    
        @Test
        @DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화")
        void test1() {
            String password = "Robbie's password";
    
            // 암호화
            String encodePassword = passwordEncoder.encode(password);
            System.out.println("encodePassword = " + encodePassword);
    
            String inputPassword = "Robbie";
    
            // 해시된 비밀번호와 사용자가 입력한 비밀번호를 해싱한 값을 비교
            boolean matches = passwordEncoder.matches(inputPassword, encodePassword);
            System.out.println("matches = " + matches); // 암호화할 때 사용된 값과 다른 문자열과 비교했기 때문에 false
        }
    }

    문자열을 암호화 하는 메서드를 정의해보자.
    @Autowired를 통해 이미 등록된 bean을 등록한다

    passwordEncoder의 .encode로 암호화를 진행한다
    matchs로 암호화를 해싱한값과 사용자가 입력한 비밀번호를 비교

  • 같은 타입의 bean이2개 이상이 있을때.
    package com.sparta.springauth.food;
    
    public interface Food {
        void eat();
    }
    
    package com.sparta.springauth.food;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class Chicken implements Food {
        @Override
        public void eat() {
            System.out.println("치킨을 먹습니다.");
        }
    }
    
    package com.sparta.springauth.food;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class Pizza implements Food {
        @Override
        public void eat() {
            System.out.println("피자를 먹습니다.");
        }
    }
     
    인터페이스 food를 상속받은 chiken과 pizza가 component 어노테이션으로 인해 자동으로 bean에 주입되었을때 
    spring은 어떤 food의 객체를 가져와야할지 모른다
    어떻게 해결해야할까?

    1. 필드명으로 명시해주기.
        @Autowired
        Food pizza;
        
        @Autowired
        Food chicken;

    이럴때는 명시를 해주면 자동으로 bean을 받아올 수 있다.
    필드명에 주입하는 방식은 직관적인 장점이 있다.

    기본적으로 Autowired는 bean을 주입할때 타입으로 찾는데
    만약에 찾지 못했을경우 필드명을 이용하여 매칭을 시도한다.

    2. primary 어노테이션 적용
    @Component
    @Primary
    public class Chicken implements Food {
        @Override
        public void eat() {
            System.out.println("치킨을 먹습니다.");
        }
    }
    
    @SpringBootTest
    public class BeanTest {
        @Autowired
        Food food;
    }

    primary 어노테이션을 사용하면 같은 타입의 bean중에 기본으로 선택될 bean을 지정한다.

    3. qualifier 사용
    @Component
    @Qualifier("pizza")
    public class Pizza implements Food {
        @Override
        public void eat() {
            System.out.println("피자를 먹습니다.");
        }
    }
    
    @SpringBootTest
    public class BeanTest {
    
        @Autowired
        @Qualifier("pizza")
        Food food;
    }

    또한 qualifier 어노테이션으로 지정해주면 주입하고자 하는 필드에 qulifier를 지정해주면 해당 객체가 주입된다.

    qulifier의 우선순위가 primary 보다 더 높다
    같은 타입의 bean이 여러개 있을때 범용적으로 사용되는 bean 객체에는 primary를 설정하고
    지엽적으로 사용되는 bnean 객체에는 qualifier를 사용한다

인증과 인가

  • 인증
    해당 유저가 실제 유저인지 확인하는것
    로그인 같은 개념

  • 인가
    해당 유저가 특정 권한을 가지고 있는지
    관리자 메뉴 같은것

  • 인증이 필요한 이유
    클라이언트와 서버의 연결문제가 있다.

    서버와 클라이언트 간의 네트워킹에는 http라는 프로토콜로 통신을하는데.
    비연결성과 무상태로 이루어진다

    비연결성
    서버와 클라이언트는 연결이 되어있지 않다는 뜻이다
    게임 같은 경우에는 실시간으로 연결상태를 확인해야 하지만
    일반적인 웹의 경우에는 그때그때 확인을 하는것이 리소스 절약에 좋은편이다
    즉, 서버는 하나의 요청에 응답을 보낸뒤 연결을 끊어놓는다고 생각하면 된다.

    무상태
    서버는 클라이언트의 상태를 저장하지 않는다는것이다
    이는 restful 원칙에도 적용되듯이 서버는 클라이언트의 그때그때의 요청에만 집중하는것이 리소스 소모를 줄일 수있다.

    그렇다면, 우리는 로그인하고 웹 페이지를 이리저리 옮겨다니는데 로그인은 유지되어있다!
    를 설명해보겠다.

  • 쿠키 - 세션방식.
    서버가 특정 사용자의 로그인 상태를 확인하기위해 최소한의 정보를 가지고 로그인을 유지시키는 방법
    출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강
    사용자가 로그인을 하면 서버는 그 회원정보를 바탕으로 로그인을 확인하고
    유저 정보와 관련이 없는 난수인 세션을 생성해주며, 클라이언트에게 보내준다.
    클라이언트는 받은 세션ID를 쿠키라는 저장소에 저장해주며,
    클라이언트는 요청을 보낼때 쿠키를 http 헤더에 함께 보내 요청을 하고
    서버는 그 쿠키를 검증하여, 해당 사용자를 판별하는것이다.

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

    세션
    서버에서 관리되는 사용자의 상태정보이다.
    출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강
    1. 클라이언트가 서버에 로그인 요청을 함
    2. 서버가 세션ID를 생성후 쿠키에 담아 헤더에 전달
    3. 클라이언트가 쿠키에 세션ID 저장
    4. 클라이언트가 서버에게 요청을함 여기서 쿠키값을 포함하여 요청함
    5. 서버가 세션 ID를 확인하고 1번과 같은 클라이언트임을 확인

    쿠키 예제
    public static void addCookie(String cookieValue, HttpServletResponse res) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
    
            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
            cookie.setPath("/");
            cookie.setMaxAge(30 * 60);
    
            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage());
        }
    }


    addCookie
    Cookie Value는 공백이 불가능하기때문에 URLEncoder로 공백을 치환해준다.
    cookie 생성자에 name과 value를 담는다
    setPath로 쿠키가 적용되는 경로를 지정하고
    MaxAge를 이용해 수명을 정한다 여기서는 1800초
    response객체에 데이터를 넣으면 클라이언트로 반환이 된다

    @GetMapping("/get-cookie")
    public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
        System.out.println("value = " + value);
    
        return "getCookie : " + value;
    }

    CookieValue 어노테이션을 사용하여 AUTHORIZATION_HEADER 이름을 가진 쿠키의 값을 가져온다.

    세션 예제
    세션은 사용자의 정보를 확인하고 세션id를 만들어줘야한다.
    @GetMapping("/create-session")
    public String createSession(HttpServletRequest req) {
        // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
        HttpSession session = req.getSession(true);
    
        // 세션에 저장될 정보 Name - Value 를 추가합니다.
        session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
    
        return "createSession";
    }

    클라이언트의 요청을 처리하기위해 HttpServletRequest 객체를 사용한다.
    HttpSesstion 객체를 만들어 getSeetion 메서드를 요청하면 세션이 존재할 경우 세션을 반환
    없을경우 새로운 세션을 만들어준다.

    setAttribute를 사용하여 name과 value를 넣어준다.


    @GetMapping("/get-session")
    public String getSession(HttpServletRequest req) {
        // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
        HttpSession session = req.getSession(false);
    
        String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
        System.out.println("value = " + value);
    
        return "getSession : " + value;
    }

    getSettion을 위와 반대로 세션이 존재할경우 세션을 반환, 세션이 없을 경우 null을 반환한다 , 새로만들 필요는 없으니까.

    또 getAttribute(name)을 하면 value값을 받아올 수 있다.

    물론 위 예제는 기본적인 session과 coockie 생성 예제이다

  • 쿠키의 문제점
    쿠키와 세션은 사용자의 로그인 상태를 유지하는데 유용하지만.

    크로스 사이트 스크립팅(XSS)와 같은 보안 취약점에 노출될 수 있다.

    공격자가 상대방의 브라우저에 스크립트가 실행되도록 유도해 사용자의 세션을 가로채거나 악의적 콘텐츠 등을 삽입하는 행위이다

    xss의 종류에는 크게 세가지 유형이 있다.
    DOM-based Xss
    클라이언트 측에서 발생하는 xss 유형이며, 웹 페이지의 DOM을 조작하여 발생한다.
    자바스크립트가 DOM을 변경하는 과정에서 악성 스크립트가 삽입된다는 것이다.
    클라이언트 쪽에서 악성 스크립트가 담긴 URL을 방물할때와 같은 경우 발생된다.
    클라이언트는 서버쪽에 데이터를 전달할때 관련 스크립트를 전송하지않고, 데이터를 전송 받았을때 해당 스크립트를 매핑하여 공격자가 데이터를 받는 유형이라 서버측에서 탐지하기는 어렵다.

    Reflected Xss
    서버에서 즉시 반영되는 사용자의 입력을 악용

    Stored Xss
    해커가 db에 악의적인 스크립트를 저장하여, 클라이언트가 악의적인 스크립트를 참조할때
    예를들어 사용자가 투명한 글씨로 악성 스크립트를 적어 댓글 같은곳에다 넣는경우, 그 글을 클릭한 클라이언트는 그 스크립트를 그대로 받게된다

  • JWT 기반 인증
    JWT는 인증에 필요한 정보를 서명한 토큰이다.
    쿠키 저장소를 통하여 JWT를 저장한다.
    JWT토큰을 HTTP헤더에 실어서 서버가 클라이언트를 식별한다

    쿠키의 문제점

    쿠키는 그때그때의 세션을 서버가 가지고 있어야한다.
    서버 1에 클라이언트의 세션 정보를 가지고있는데 서버2나 3에 요청을한다면?
    그러기 위해서 세션 스토리지를 두었다.

    하지만 세션은 그때그때 사용할 각 유저의 세션을 위해 데이터베이스에 저장해야한다
    JWT 토큰은 서버에서 생성 되어 클라이언트에 보낸후 클라이언트에만 정보를 보관한다
    서버는 토큰을 클라이언트에 전달할때 암호화, 받은 JWT토큰을 원래 값과 같은지 검증하는것이다.


    서버는 무결성 검증만 수행하여 부담을 줄이게 된다. 
    사용자가 로그인을 하면 서버는 그 회원정보를 바탕으로 로그인을 확인하고
    유저에 대한 정보를 JWT로 암호화를 하여, 응답에 실어서 보내준다.
    클라이언트는 데이터요청을 했을때 JWT를 실어서 보낸다
    서버는 JWT를 검증하여 응답을 내준다

    출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강
    • JWT의 흐름
      클라이언트가 로그인을 요청
      서버는 비밀키를 사용하여 JWT로 암호화
      서버에서 JWT를 담아 클라이언트 응답에 전달함
      로그인 완료
      클라이언트에서 API 요청
      서버는 받은 데이터에서 JWT토큰 분석
      토큰이 일치하면 데이터를 반환해줌

    • JWT의 장단점
      장점
      Stateless(무상태) 인증을 가능하게 한다. 서버는 사용자의 정보를 관리 할 필요가 없게되므로 메모리 사용량이 줄어든다
      클라이언트와 서버가 다른 도메인을 사용할때 

      단점
      구현의 복잡도가 증가한다
      네트워크에 담기는 커진다는 단점
      무상태이기 때문에 서버에서 무효화가 어려운 문제가 있다.
      key 유출시 조작 가능

    • 구현 방법
      클라이언트에서 jwt를 통해 인증하는 방법
      // HttpServletRequest 에서 Cookie Value : JWT 가져오기
      public String getTokenFromRequest(HttpServletRequest req) {
          Cookie[] cookies = req.getCookies();
          if(cookies != null) {
              for (Cookie cookie : cookies) {
                  if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                      try {
                          return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                      } catch (UnsupportedEncodingException e) {
                          return null;
                      }
                  }
              }
          }
          return null;
      }

      쿠키에 담긴 정보가 여러개일 수 있기때문에 쿠키를 순회하며
      AUTHORIZETION_HEADER과 동일한지 검증한다.

    • JWT 구현해보기
      build gradle에 아래와 같이 적어준다
      // JWT
      compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
      runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
      runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
      application.properties
      jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

      BASE64로 인코딩한 secret key를 사용한다


      jwtUtil 만들기
      Util 클래스 컨벤션은 특정 매개변수에 대한 작업을 하는 메서드들이 존재한 클래스의 집합을 정의할때 쓰인다
      jwtUtil을 만들어 jwt기능만을 수행하는 클래스를 만든다.

      jwtUtil의 기능
      토큰 생성에 필요한메서드 들이 존재하는 클래스를 만들어 한꺼번에 관리하는 클래스이다

      1. JWT 생성
      jwt subject에 사용자의 id(식별자)를 넣는다
      권한 정보를 넣는다
      토큰 만료시간을 ms 기준으로 넣는다
      발급일과
      암호화 알고리즘 값을 넣는다
      // 토큰 생성
      public String createToken(String username, UserRoleEnum role) {
          Date date = new Date();
      
          return BEARER_PREFIX +
                  Jwts.builder()
                          .setSubject(username) // 사용자 식별자값(ID)
                          .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                          .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                          .setIssuedAt(date) // 발급일
                          .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                          .compact();
      }

      jwt를 보내는 방법의 두가지
      a. 쿠키를 서버에서 직접 만들고 직접 보내면, 만료기한 다른 옵션도 추가할 수 있다.
      b. 헤더에 넣는 방법은 쿠키를 만들 필요가 없이, 토큰을 헤더에 담으면 된다, 코드수가 줄수 있다.

      2. JWT 쿠키에 저장

      3. 받아온 쿠키의 Value 인 JWT토큰 가져오기
      토큰의 null을 확인하고 토큰값을 받아온다

      4. JWT 검증
      Jwts.parserBuilder()을 사용하여 jwt를 파싱한다
      jwt가 위변조 되지 않았는지 secretKey를 넣어 확인한다

      5. JWT에서 사용자 정보 가져오기
      payload에 있는 토큰 정보를 읽는다
      Jwts.parserBuilder()를 사용하여 파싱하고, secretKey를 사용하여 사용자 정보를 조회한다.

회원가입 구현

모든 코드를 올리기에는 상도덕에 어긋남 ㅇㅇ 궁금한점만 잘라서 올리도록 한다

  • entity 어노테이션
    @Enumerated(value = EnumType.STRING) //USER, ADMIN
    이넘의 타입을 결정해준다.
    STRING을 설정한다면 USER 그대로 저장한다.

  • 패스워드 암호화 로직
    회원 등록시 비밀번호는 사용자가 입력한 그대로 DB에 등록하면 안된다
    정보통신망법, 개인정보보호법에 의거한..

    해커가 db를 갈취하더라도 비밀번호 정보는 알 수 없다.
    복호화 하기 힘든 단방향 암호 알고리즘이 주로 사용된다.

  • 암호화 알고리즘
    양방향 암호 알고리즘 :
    암호화 : 평문 > 암호화 알고리즘 > 암호문
    복호화 : 암호문 > 암호화 알고리즘 > 평문

    단방향 암호 알고리즘
    암호화 : 평문 > 암호화 알고리즘 > 암호문

    DB의 데이터가 탈취되더라도, 비밀번호를 복호화 불가능한 단방향 알고리즘을 사용하면, 찾을수 없다.
    spring security 프레임워크에 비밀번호 암호화기능과, 저장된 비밀번호와 비교를하는 코드도있다.
    if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) {
    		   throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
     }


회원가입 구현

private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

현재는 관리자 토큰이 이렇게 되어있지만,

현업에서는 이렇게 쓰지않는다, 관리자 페이지나, 승인자에 의한 결재과정으로 된다.

지금은 테스트!

return "redirect:/api/user/login-page?error=" + e.getMessage();

 

redirect에 error을 보내주면 에러메세지를 같이 반환함

 

필터

  • 필터란?
    클라이언트로 부터 오는 요청과 응답에 대해, 최초/최종 단계의 위치이며 이를 통해 요청과 응답의 정보를 변경하거나, 부가적인 기능을 추가 할 수 있음.
    인증, 인가와 관련된 로직을 처리 할 수 있음
  • 필터 만들기
    로깅 필터 만들기
    @Slf4j(topic = "LoggingFilter")
    @Component
    @Order(1)
    public class LoggingFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 전처리
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String url = httpServletRequest.getRequestURI();
            log.info(url);
    
            chain.doFilter(request, response); // 다음 Filter 로 이동
    
            // 후처리
            log.info("비즈니스 로직 완료");
        }
    }

    filter를 component 어노테이션을 달고
    filter도 chain이라는 순서가 있다, order라는 어노테이션으로 순위를 매겨준다.
    필터 인터페이스를 상속한다

    request와 response, 다음 필터로 이동하기위한 FilterChain

    어떻게 수행되는가?
    http요청이 들어오면 filter에서 전처리를 수행한다 >> 어떤 로그인지 출력
    다음 필터로 이동시킨다.

    인증 인가  필터 만들기

    LoggingFilter에서 doFilter를 한다면 다음 order인 AuthFilter로간다.
    @Slf4j(topic = "AuthFilter")
    @Component
    @Order(2)
    public class AuthFilter implements Filter {



 


오늘의 회고 & 12시간 몰입했는가? 


찾아볼 내용

'TIL' 카테고리의 다른 글

20240822 본캠프 29일차 TIL  (0) 2024.08.22
20240821 본캠프 28일차 TIL  (0) 2024.08.21
20240817 주말에도 나와버린 나 TIL  (0) 2024.08.17
20240816 내배캠 25일차 TIL  (0) 2024.08.17
20240815 본캠프 24일차 TIL  (0) 2024.08.15