오늘은 학원에서 JWT 토큰 기반의 인증 방식에 대해서 알아보았다.
토큰 기반의 인증 방식 (JWT)
토큰 인증 방식 :
원래 예전부터 요청을 처리하는 인증방식에는 세션을 사용한 방법을 사용했는데
이게 사용자가 많아지면 많아질수록 서버에 부하가 많아져서 성능에 무리가 간다.
그래서 나온게 토큰 인증 방식.
그 중에 지금은 JWT 를 사용하기로 함.
JWT 란 ?
JSON 객체를 사용하여 토큰을 사용하는 토큰 형식
정보를 안전하고 간단하게 주고받기 위해 사용되는 표준
JWT 는 세 부분으로 구성된다.
- 헤더(Header)
- 토큰의 타입과 사용된 암호화 알고리즘을 포함
- 페이로드(Payload)
- 서명(Signature)
JWT의 동작원리
- 사용자가 id와 password를 입력하여 로그인 요청을 한다.
- 서버는 회원DB에 들어가 있는 사용자인지 확인을 한다.
- 확인이 되면 서버는 로그인 요청 확인 후, secret key를 통해 토큰을 발급한다.
- 이것을 클라이언트에 전달한다.
- 서비스 요청과 권한을 확인하기 위해서 헤더에 데이터(JWT) 요청을 한다.
- 데이터를 확인하고 JWT에서 사용자 정보를 확인한다.
- 클라이언트 요청에 대한 응답과 요청한 데이터를 전달해준다.
JWT 사용하는 이유
일반 토큰 기반 vs 클레임 토큰 기반
JWT를 사용하는 가장 큰 이유는 클레임(Claim) 토큰 기반 인증이 주는 편리함이 가장 크다고 할 수 있다. 과연 일반 토큰 기반과 클레임 토큰 기반 인증의 차이는 무엇일까?
기존에 주로 사용하던 일반 토큰 기반 인증은 토큰을 검증할 때 필요한 관련 정보들을 서버에 저장해두고 있었기 때문에 항상 DB에 접근해야만 했었다. 또한 session방식 또한 저장소에 저장해두었던 session ID를 찾아와 검증하는 절차를 가져 다소 번거롭게 느껴지곤 했다🤣
하지만 클레임 토큰 기반으로 이루어진 JWT(Json Web Token)는 사용자 인증에 필요한 모든 정보를 토큰 자체에 담고 있기 때문에 별도의 인증 저장소가 필요없다. 분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는 쉬운 인증을 제공하여 일반 토큰 기반 인증에 비해 편리하다고 말할 수 있다.
수업을 듣다가 코드에서 Authentication 과 authorization 이라는 단어가 자주 나와서 좀 헷갈려서 여기 정리해놓는다.
Authorization 과 Authentication 의 차이 ?
- Authentication (인증) : 당신은 누구인가 ?
- 사용자가 주장하는 신원이 실제로 맞는지를 확인
- 사용자 이름과 비밀번호 입력
- 소셜로그인 , 인증 토큰 등 사용
- 결과적으로 사용자 인증에 성공하면 시스템은 사용자를 식별할 수 있게 된다.
- Authorization (인가, 권한 부여) : 당신은 무엇을 할 수 있는가 ?
- 인증된 사용자가 어떤 작업에 접근할 수 있는지를 확인
- 사용자의 역할이나 권한을 기반으로 접근을 제어.
- 기본적으로 인증이 완료된 이후 수행된다 !
JWT 사용법
우선 필요한 라이브러리를 추가해준다.(Gradle 프로젝트 기준)
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
JwtTokenProvider
@RequiredArgsConstructor
@Component @Slf4j
public class JwtTokenProvider {
@Value("${security.jwt.secret-key}")
private String SECRET_KEY;
private final long tokenValidTime = 30 * 60 * 1000L; //토큰 유효시간 -> 30분
private final UserService userService;
//객체 초기화, secretKey를 Base64로 인코딩
@PostConstruct
protected void init() {
SECRET_KEY = Base64.getEncoder().encodeToString(SECRET_KEY.getBytes());
}
//토큰 생성
public String createToken(String adminPk) {
//adminPk => loginId
Claims claims = Jwts.claims().setSubject(adminPk); //JWT payload 에 저장되는 정보단위
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) //토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) //토큰 유효시각 설정
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) //암호화 알고리즘, secret 값 설정
.compact();
}
//인증 정보 조회
public Authentication getAuthentication(String token) {
//Spring Security에서 제공하는 메서드 override해서 사용해야 함
AdminLoginDto adminLoginDto = userService.loadUserByUsername(this.getAdminPk(token));
return new UsernamePasswordAuthenticationToken(adminLoginDto.getAdmin(), "", adminLoginDto.getAuthorities());
}
//토큰에서 Admin 정보 추출
public String getAdminPk(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
}
//토큰 유효성, 만료일자 확인
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
log.debug(e.getMessage());
return false;
}
}
//Request의 Header에서 token 값 가져오기
public String resolveToken(HttpServletRequest request) {
return request.getHeader("Authorization");
}
}
토큰 생성, 인증, 인증정보 조회 등 토큰 관련 로직이 구현되어 있다.
secret key는 원하는 값으로 지정해 application.properties 파일에 설정해주면 된다.
createToken 메서드는 토큰을 생성하는 부분이다. claims에 권한 등 원하는 정보를 추가로 put할 수 있다.
JWT 는 어디에 저장할 수 있는가 ?
JWT를 저장하는 장소에는 2가지 방법이 있다.
- 로컬 스토리지
- 쿠키
각 저장소의 특징
1. localStorage에 저장
👍 장점
CSRF 공격에는 안전하다.
그 이유는 자동으로 request에 담기는 쿠키와는 다르게
js 코드에 의해 헤더에 담기므로 XSS를 뚫지 않는 이상
공격자가 정상적인 사용자인 척 request를 보내기가 어렵다.
👎 단점
XSS에 취약하다.
공격자가 localStorage에 접근하는 Js 코드 한 줄만 주입하면
localStorage를 공격자가 내 집처럼 드나들 수 있다.
2. cookie에 저장
👍 장점
XSS 공격으로부터 localStorage에 비해 안전하다.
쿠키의 httpOnly 옵션을 사용하면 Js에서 쿠키에 접근 자체가 불가능하다.
그래서 XSS 공격으로 쿠키 정보를 탈취할 수 없다.
(httpOnly 옵션은 서버에서 설정할 수 있음)
하지만 XSS 공격으로부터 완전히 안전한 것은 아니다.
httpOnly 옵션으로 쿠키의 내용을 볼 수 없다 해도
js로 request를 보낼 수 있으므로 자동으로 request에 실리는 쿠키의 특성 상
사용자의 컴퓨터에서 요청을 위조할 수 있기 때문.
공격자가 귀찮을 뿐이지 XSS가 뚫린다면 httpOnly cookie도 안전하진 않다.
👎 단점
CSRF 공격에 취약하다.
자동으로 http request에 담아서 보내기 때문에
공격자가 request url만 안다면
사용자가 관련 link를 클릭하도록 유도하여 request를 위조하기 쉽다.
정리
우선 JWT 를 사용하는 이유는 특정 DB 에 의존하지 않아도 되고 서버의 확장성이 높아서 사용함.
대강적인 흐름
로그인을 요청하면 서버는 JWT 토큰을 생성한다.
생성 후에 클라이언트에게 전달 . 토큰값은 보통 쿠키나 Authorization 헤더에 저장됨.
authentication 객체는 주로 사용자의 인증정보를 포함하고 있음.
이놈은 securityContext 에 저장되어서 요청마다 자동으로 처리된다.
코드로 보면
//JWT TOKEN 생성 + 쿠키 전달
TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);
System.out.println("토큰 인포 ? : "+tokenInfo);
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME,tokenInfo.getAccessToken());
cookie.setMaxAge(JwtProperties.EXPIRATION_TIME);
cookie.setPath("/");
response.addCookie(cookie); //쿠키 저장
log.info("CustomLoginSuccessHandler's onAuthentication'");
response.sendRedirect("/");
로그인 요청을 하고 나면 토큰값을 생성하는데 이 토큰을 쿠키에 저장한다. 그리고 인덱스 페이지로 리다이렉팅.
그리고 후에 사용자가 로그아웃을 요청하면 logout핸들러가 실행되는데 , 이 때 authentication 에서 사용자 정보에 접근할 수 있다. 그런데 이때 로그아웃 처리 시점에서 authentication 객체가 null로 나올 수 있다.
이 때 객체가 null 이라면 jwt 토큰을 사용해서 인증 정보를 복원하게 되는데 ,
(객체를 복원하는 이유 ? :
authentication 객체는 로그인한 사용자의 인증정보를 저장하고 있는 객체인데 , 로그아웃을 처리하려면 (근데 로그인을 하면 이 객체에 이미 정보가 담겨있는거 아닌가 ? 왜 null이 되는거지
서명
서명 리포지토리를 생성
db테이블을 만들고
프로그램 실행해서 테이블 생성
로그인을 하면 토큰이 생성됨. 이걸 지우고 프로그램을 껏다 켜도 db에 서명값이 저장되있어서
리멤버미 기능을 사용하는것처럼 그대로 정보가 남아있는걸 볼수있음
이거를 일정 시간마다 갱신되게 하는 작업을 했음
package com.example.demo.config.auth.scheduled;
import com.example.demo.config.auth.jwt.JwtTokenProvider;
import com.example.demo.config.auth.jwt.KeyGenerator;
import com.example.demo.domain.entity.Signature;
import com.example.demo.domain.repository.SignatureRepository;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.time.LocalDate;
import java.util.List;
@Component
@EnableScheduling
public class SignatureScheduled {
@Autowired
private SignatureRepository signatureRepository;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Scheduled(cron="0 0 0 * * *")
public void t2() {
System.out.println("Scheduling's t2() invoke....");
List<Signature> list = signatureRepository.findAll();
if(list.isEmpty()) { //db에 저장된 key가 없으면}
byte[] keyBytes = KeyGenerator.getKeygen(); //키 생성한 값 받아옴
Signature signature = new Signature(); //리포지토리 생성
signature.setKeyBytes(keyBytes); //db에 저장
signature.setCreate_At(LocalDate.now());
signatureRepository.save(signature); //리포지토리에 저장
Key key = Keys.hmacShaKeyFor(keyBytes); //키 암호화 시킴
jwtTokenProvider.setKey(key);
System.out.println("스케줄 키 인잇");
}else{
Signature signature = list.get(0); //데이터를 꺼냄
byte[] keyBytes = signature.getKeyBytes();
signature.setKeyBytes(keyBytes); //db에 저장
signatureRepository.deleteAll();
signatureRepository.save(signature); //리포지토리에 저장
Key key = Keys.hmacShaKeyFor(keyBytes); //키 암호화 시킴
jwtTokenProvider.setKey(key);
System.out.println("스케줄 키 체인지");
}
}
}
일정 시간마다 서명값이 새로 생성되게 함.
초분시일연월 ?
'Developer Note > 국비과정 수업내용 정리&저장' 카테고리의 다른 글
24년 12월 6일 (2) | 2024.12.15 |
---|---|
24년 12월 5일 (1) | 2024.12.15 |
24년 12월 3일 (0) | 2024.12.15 |
24년 12월 2일 (2) | 2024.12.10 |
24년 11월 28일 (1) | 2024.12.08 |