[Spring Boot] 60. 스프링부트 블로그 v3 (RestAPI) (5) 해시와 Access Token

김주희's avatar
May 08, 2025
[Spring Boot] 60. 스프링부트 블로그 v3 (RestAPI) (5) 해시와 Access Token
수업 요약
1.
2.
3.
4.
5.
 
복습 UTF-8 유니코드에서 만든 전세계 공통 문자 인코딩 방식 1-4Byte 가변 크기(영어 1Byte, 한글 3Byte, 이모지 4Byte) → 조합형 문자 DB → Table(UTF-8) MySQL은 3Byte까지만 (자기만의 utf-8표가 있음) → 4Byte가 없음 ⇒utf-8(mb4)을 만들어서 4Byte 가능하게 만듦(이모지 가능) = 표준 utf-8 Base64 목적(등장배경) = JSON에 이미지를 포함시키려고(데이터 한방 전송) JSON에 Byte를 포함할 수 없다. 이미지 -> 문자 (250 값 -> 아스키 코드에서 못찾음) 6bit 짤라서 Base64표에서 문자를 찾아냄 그 문자를 다시 아스키코드에서 찾아냄 결국 전송은 8bit로 한다. -> 비효율적인 로직들이 있지만 안전하고 2번 통신하는 것보다 낫다 전자서명 1. 양방향(Encoding, Decoding), 단방향(Encoding) 2. 양방향 방식 (대칭키, 공개키) 3. 단방향 방식 (해시) -> 서명
 
notion image
톰캣쪽에 utf-8 설정
외부에서 들어오는 애가 짐을 지고 들어와.. spring이라는 성에 들어올 때 반드시 거치는게 was
톰캣은 8080 같은 문의 번호를 들고 있음
0~65535 개 가능 - inbound 정책(문을 닫아둠) ↔ outbound
아무튼 문지기가 web.xml을 읽음 → = application.properties
짐 = body 데이터
짐의 표지에 레이블링을 해야 됨 = 그 안의 데이터가 뭔지 표시해둬야 됨 ⇒ 사과라고 써둠(content-type 같은것)
?
OS레벨에[서 패킷의 번호 등을 조합해서 순서 정하고 데이터를 조합해서 딱 만들어내
데이터가 들어와서 바이트로바꿔낸걸 OS에서 app로 1바이트씩 올림(1바이트짜리 바이트 스트림이니께)
app레벨에서 바이트 몇개씩 묶어서 의미있는 데이터네 해야됨(→ utf-8인지에 따라 문지기가 3바이트씩 끊어서 읽음 ⇒ 의미있는 데이터가 됨)
어차피 전부 utf-8로 처리해야하니까 입구에서 처리하는게 편함
force ⇒ 들어오는 사람?이 utf-8 말고 다른걸로 커스텀 설정해도 막는것
 

1234를 해시하면 예를 들어 5A3D가 나옴 1234를 한번더 해도 똑같이 나오고 복호화는 불가능하다. → 전자서명
1234를 대칭키로 잠그면 예를 들어 38A7가 나옴 대칭키만 있으면 복호화 가능하다. → 키를 들고 있는 사람이 키를 외부에 배포하면 안되는 것 - 키를 전달하면 전달하다가도 문제가 생길 수 있음 → 전자서명 가능은 하지만 전자서명을 확인할 수 있는게 본인뿐이여서 비효율적
1234를 (공개키)로 잠그면 예를 들어 89P5가 나오고 방식 2가지
  1. 상대방의 공개키 → 상대방만 열어볼 수 있음 → 암호화
  1. 나의 개인키 → 모든 사람이 열어볼 수 있음 → 전자서명
 
로그인 - db에 있네? → id(개인정보가 아니고 식별 불가한 것)만 종이에 적어서
해시는
복호화가 안돼서 안될거라고 생각하는데
원본데이터 id1은 두고 그걸 해시한 값도 추가해서 함께 전달 → 원본 데이터 보고 해시해서 맞는지 확인 가능
해시만 주면 복호화도 안되고 다시 해시해볼수 있는 원본데이터 없으니까 안됨

대칭키
원본 데이터 있고 잠근 데이터도 같이 붙엇어
열고 비교해봐야됨.. 원본데이터 확신 못함 값을 검증해봐야 됨
 
원본 데이터 + 잠궈서 생긴 난수 → 원본을 바꿈 → 다시 줌 → 잠근걸 열어보니까 복호화가 돼서 다른걸 비교 가능 → 인증 못함 →
 
공개키
상대방의 공개키는 누구나 암호화 할 수 있기 때문에
남의 공개키로 내 서버에서 서명하면 남의 비밀키로 열어봐야 하기 때문에
나의 공개키로 잠구면 나만 열 수 잇어서 이것도 안됨
 
나의 개인키
서버의 비밀키로 잠구면 이것도 가능 신뢰할 수 잇따.
 
우리는 해시 쓸 것
 
1. 로그인 2. 유저 객체를 가지고 토큰 생성 SHA256 ID:1 해시(SHA256-ID:1-Metacoding) -> 98AB3 JWT 완성 (SHA256).(ID:1).(98AB3) 3. 클라이언트가 토큰 들고 인증이 필요한 API 요청 4. (SHA256).(ID:1) + 시크릿 이걸로 해시 5. (98AB3)와 동일한지 검증 => 인가
 
 
 

코드짜기 - 샘플링 테스트
notion image
 
 
notion image
notion image
→ gensalt()에 의해 매번 달라지므로
 
 
notion image
notion image
 
 
 
 

회원가입 → 해시로

notion image
notion image
notion image
 
 
 

로그인

더미데이터도 수정
notion image
 
notion image
notion image
 
 
올바른 로그인의 경우
notion image
 
비밀번호가 틀린 경우
notion image
 

token 생성 및 검증 테스트
 
  1. 토큰 생성
라이브러리 땡기면 됨
내부적으로 일어나는 일은
with 4가지는 payload에 저장되는 애
만료시간 = 현재시간+(내가한 설정은 1시간짜리)
시간에 대해서는 나중에 공부(정리 필요)
사용자를 식별할 수 있는 정보 하나만 넣으면 됨 pw X 누구나 열어볼 수 있으므로
header에는 Algorithm.HMAC256 가 들어간다
notion image
notion image
 
 
notion image
notion image
 
  1. 토큰 검증
헤더 페이로드를 시크릿값과 함께 해시해서 제일 끝에있는 sign과 동일한지 확인
확인하고 나서 expire → 시간만료 체크해서 만료되면 exception 터짐
 
 
 
 
 
notion image
 
 
notion image
notion image
notion image
 
notion image
→ 이제 인증이 필요한 페이지에 갈때는 accessToken을 들고 가야 됨
인가?
 
인터셉터 vs 필터 어디서?
둘다 /s 가 붙은데에서만 검증 후 입장 가능
필터에서 throw 날릴 ㅜㅅ 잇으니까 여기서 하자
인터셉터에서 해도 되는데
 
f - ds - interceptor - c - s - r - db
입구에서 하는게 낫다..ds이후에서 하면 reflection이 엄청나니까
둘다 가능하면 그냥 앞에서 하는게 ㅏㄴ음
뒤에서 하면 돈 많이 나옴 → controller 찾는 일함
notion image
 
 
 

Authorization이라는 프로토콜에 맞춰서 하는게 낫다 프로토콜 안지키고 내맘대로 가능은 하지만 안보여서 보이도록 설정도 해줘야되고 암튼 귀찮아지니까 프로토콜에 맞춰서하자
 
f에서 검증해서 jwt에 있는 정보 꺼냄 = 인증됨 → c에 들어옴 → c에서 user id 같은거 필요한 경우 생김 → 다시 검증하는 것보다는 request가 계속 넘어가니까 거기에 담아서 보내는게 나음(세션도 가능은 한데 세션 안ㅆ느다면서 왜 써요 같은 질문 많아서 안씀)
→ 근데 session에 넣어두면 기존의 session 코드 수정 안해도 되니까 session 다시 쓰자..!
session은 잠깐 꺼낼때 쓰는 거임!(다시 검증하기 번거로우니까 넣어두는 용도)
 
package shop.mtcoding.blog._core.filter; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import shop.mtcoding.blog._core.error.ex.ExceptionApi401; import shop.mtcoding.blog._core.util.JwtUtil; import shop.mtcoding.blog._core.util.Resp; import shop.mtcoding.blog.user.User; import java.io.IOException; import java.io.PrintWriter; public class AuthorizationFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String accessToken = request.getHeader("Authorization"); if (accessToken == null) throw new ExceptionApi401("토큰 전달해야지!! 짜식아!!"); if (!accessToken.startsWith("Bearer ")) throw new ExceptionApi401("프로토콜 지켜야지!! 짜식아!!"); accessToken = accessToken.replace("Bearer ", ""); try { User user = JwtUtil.verify(accessToken); request.setAttribute("requestUser", user); chain.doFilter(request, response); } catch (TokenExpiredException e1) { exResponse(response,"토큰이 만료되었습니다"); } catch (JWTDecodeException e2) { exResponse(response, "토큰 검증에 실패했어요"); } } private void exResponse(HttpServletResponse response, String msg) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); Resp<?> resp = Resp.fail(401, msg); String responseBody = new ObjectMapper().writeValueAsString(resp); out.println(responseBody); } }
notion image
 
 
 
 
 

response.setStatus(400); 하면 상태코드도 변경됨 (원래는 200에 내가 그냥 400을 던진 것)
ResonseEntity를 써서
 
 

controller에서는 responseEntity를 안해도 되는 이유? → 정상적으로 가면 어차피 200 오류가 생기면 어차피 전부 exception 터지니까
 
 

postman으로
1.ssar로 로그인해보기
→ accessToken 복사해두기(1시간 유효)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJibG9nIiwiaWQiOjEsImV4cCI6MTc0Njc2NjAzMCwidXNlcm5hbWUiOiJzc2FyIn0.X3NQEKpehsYHT4J2XyCegjsfcAZCR8ErBmyjycJaTsSnRfGnLxULvFLDYYK3--xGhWPy0IjBuqrl3oDiUhurJg
 
2.댓글 삭제 테스트
-토큰 안넣고 삭제
-토큰 넣고 삭제
-권한이 없는 거 삭제
 
권한이 없는 거 삭제
notion image
토큰 안넣고 삭제
notion image
 
 
토큰 넣고 삭제
notion image
 
 
토큰 잘못된거 넣고 삭제
notion image
 
 
 
notion image
Share article

jay0628