# 인증
서버가 클라이언트 인증을 확인하는 방법은 3가지가 있다.
- Cookie
- Session
- JWT
## Cookie
쿠키는 Key - Value 구조이다.
쿠키는 클라이언트의 브라우저에 저장된다.
인증 방식
- 클라이언트는 브라우저를 통해 서버에 요청을 보낸다.
- 서버는 클라이언트의 요청에 대한 응답을 보낼 때, 클라이언트 측에 저장할 정보를 응답 헤더의 Set-Cookie에 담아 보낸다.
- 클라이언트는 요청을 보낼 때마다, 저장된 쿠키를 요청 헤더의 Cookie에 담아 보낸다.
- 서버는 쿠키에 담긴 정보를 바탕으로 클라이언트를 식별한다.
단점
- 쿠키의 값을 그대로 보내기에 보안에 취약하다.
- 용량 제한이 있다.
- 브라우저마다 쿠키에 대한 지원 형태가 다르기에 브라우저간 공유가 불가능하다.
- 쿠키 사이즈가 클수록 네트워크 부하가 심해진다.
## Session
세션은 Key에 해당하는 Session Id와 Value로 구성된다.
쿠키의 보안 이슈를 보완하기 위해, 세션은 비밀번호 등 클라이언트의 민감한 정보를 브라우저가 아닌 서버에서 관리한다.
인증방식
- 클라이언트가 서버에 접속하면 세션이 서버 측에 저장된다.
- 서버에서 브라우저 쿠키에 Session ID를 저장한다.
- 쿠키에 Session ID가 담겨있기에, 클라이언트는 서버에 요청 시 Session ID를 쿠키에 담아 전송한다.
- 서버는 클라이언트가 보낸 Session ID와 서버 측에서 관리하는 Session ID를 비교하여 인증을 수행한다.
단점
- Session ID를 담고 있는 쿠키가 노출되어도 ID기에 유의미한 개인 정보를 담고 있지 않는다.
- 하지만, 해커가 세션 ID 자체를 탈취하여 클라이언트 인척 위장이 가능하다.
- 서버에서 세션 저장소를 사용하므로 요청이 많아지면 서버 부하가 증가한다.
## Token
토큰 기반 인증 시스템은 클라이언트가 서버에 접속하면 서버에서 해당 클라이언트에게 토큰을 부여한다.
토큰은 클라이언트에 저장된다.
Session vs Token
세션은 서버의 램 또는 데이터베이스에서 사용자의 인증 정보를 관리한다.
그렇기에, 클라이언트로부터 요청을 받으면 클라이언트의 상태를 계속해서 유지해놓고 사용한다.(Stateful)
이러한 특징 때문에 사용자가 증가하면 성능 문제가 발생할 수 있으며, 확장성이 어렵다.
토큰 발급은 서버에서 하지만 관리는 클라이언트에서 한다.
토큰은 세션과 달리 토큰 자체에 데이터가 들어있기에, 클라이언트에서 전달받아 위조되었는지만 판단하면 된다.
그렇기에, 상태를 유지할 필요가 없다.(Stateless)
인증 방식
- 클라이언트가 로그인을 한다.
- 서버에서 클라이언트에게 유일한 토큰을 발급한다.
- 클라이언트는 발급 받은 토큰을 쿠키나 스토리지에 저장하고, 서버에 요청할 때마다 HTTP 헤더에 포함하여 전달한다.
- 서버는 전달 받은 토큰을 검증하고 요청에 응답한다.
- 토큰 자체에 클라이언트에 대한 정보가 담겨있기에 서버는 DB 조회 없이 누가 요청했는지 알 수 있다.
단점
- 쿠키 & 세션과 다르게 토큰 자체의 데이터 길이가 길어, 인증 요청이 많을수록 네트워크 부하가 심해진다.
- Payload 자체는 암호화되지 않기 때문에 유저의 중요한 정보는 담을 수 없다.
- 토큰을 탈취당하면 대처가 어렵다. (이를 위해 expire 설정 필요)
# JWT
JSON Web Token
인증에 필요한 정보들을 암호화시킨 JSON 토큰이다.
## JWT 구조
Header, Payload, Signature
Header
JWT에서 사용할 타입과 해시 알고리즘의 종류가 담겨있다.
{
"alg": "HS256",
"typ": "JWT"
}
- alg : 서명 암호화 알고리즘
- typ : 토큰 유형
Payload
토큰에서 사용할 정보의 조각들인 Claim이 담겨있다.
즉, 서버와 클라이언트가 주고받는 시스템에서 실제 사용될 정보에 대한 내용이다.
Claim
key - value 형태로 이루어진 한 쌍의 정보
{
"jti": "1000", // Registered Claim
"sub": "sample", // Registered Claim
"exp": "1521430000000", // Registered Claim
"lat": "1516213121", // Registered Claim
"https://sample.com": true, // Public Claim
"username": "hoooon" // Private Claim
}
- Registered Claims : 미리 정의된 클레임
- iss : 발행자
- exp : 만료시간
- sub : 제목
- iat : 발행 시간
- jti : jwt id
- Public Claims : 사용자가 정의할 수 있는 클레임, 공개용 정보 전달
- Private Claims : 해당하는 당사자들 간에 정보를 공유하기 위해 만들어진 클레임, 외부에 공개돼도 상관없지만 해당 유저를 특정할 수 있는 정보를 담는다.
Signature
헤더와 페이로드, 서버가 갖고 있는 유일한 key 값을 합친 것을 헤더에서 정의한 알고리즘으로 암호화 한 것이다.
Header와 Payload는 단순히 인코딩된 값이기에 제 3자가 복호화 및 조작할 수 있지만,
Signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없다.
즉, Signature는 토큰의 위변조 여부를 확인하는데 사용된다.
## JWT 인증 방식
- 클라이언트가 로그인 인증을 요청한다.
- 서버에서 인증 요청을 받으면 Header, Payload, Signature를 정의한다.
- 서버는 정의한 Header, Payload, Signature를 각각 Base64로 암호화하여 JWT를 생성한다.
- 서버는 JWT를 쿠키에 담아 클라이언트에게 발급한다.
- 클라이언트는 발급 받은 JWT를 로컬 스토리지에 저장한다.
- 이후, 서버에 요청할 때마다 Authorization Header에 Access Token을 담아 보낸다.
- 서버는 클라이언트가 Header에 담아 보낸 JWT가 서버에서 발행한 토큰인지 확인한다.
- 서버에서 발행한 토큰이라면 인증 통과를 시키고, Payload에 들어있는 유저 정보를 select 하여 클라이언트에게 돌려준다.
- 클라이언트가 서버에 요청했는데, Access Token 시간이 만료되었다면, 클라이언트는 Refresh Token을 이용하여 Token 재발급을 요청한다.
- 서버는 새로운 Access Token을 발급해준다.
JWT의 진짜 목적
JWT는 Base64로 암호화 하기에 쉽게 복호화할 수 있다.
그렇기에 Payload에 비밀번호 같은 민감한 정보는 넣지 말아야 한다.
이런 점으로 보았을 때, 토큰은 정보 보호가 아니라 위조 방지가 진짜 목적인 것이다.
Signature에 사용된 비밀키가 노출되지 않는 이상 데이터를 위조해도 Signature 부분에서 바로 걸러지기 때문이다.
## JWT 장점
- Header와 Payload를 가지고 Signature를 생성하므로 데이터 위변조를 막을 수 있다.
- 인증 정보에 대한 별도 저장소가 필요 없다.
- 토큰 자체에 기본 정보와, 전달 정보, 서명 등이 들어 있다.
- 클라이언트 인증 정보를 저장하는 세션과 다르게 Stateless 하다.
- 확장성이 우수하다.
- 다른 로그인 시스템에 접근 및 권한 공유가 가능하다.
## Refresh Token
Access Token 만을 통한 인증 방식은 토큰 자체를 탈취당할 경우 보안에 취약하다는 단점이 있다.
즉, Access Token은 발급 이후에 서버에 저장되지 않고 토큰 자체로 검증을 하기에, 해당 토큰이 만료되기 전까지 악용이 가능해진다.
JWT는 발급 후 삭제가 불가능하기에, 유효시간을 부여하여 탈취 문제에 대응해야 한다.
하지만, 유효시간을 너무 짧게 하면 로그인을 그만큼 자주 하여 새로운 JWT를 발급받아야 하는 불편함이 존재한다.
이때, 사용하는 것이 Refresh Token이다.
Access Token은 접근에 관여, Refresh Token은 재발급에 관여하는 JWT이다.
인증 방식
- 클라이언트가 로그인 인증을 요청한다.
- 서버는 Access Token과 Refresh Token을 동시에 발급하고 Refresh Token만 서버 측에 저장한다.
- 이때, Refresh Token의 유효기간이 Access Token보다 길다.
- 클라이언트는 Access Token과 Refresh Token 모두를 스토리지에 저장한다.
- 그리고 요청이 있을 때마다 두 개의 Token 모두를 Header에 담아 보낸다.
- 클라이언트가 요청할 때, Access Token이 만료되었다면, 서버는 같이 온 Refresh Token을 서버 측에서 보관한 것과 비교하여 Access Token을 재발급한다.
- 그리고 클라이언트가 로그아웃하면 Access Token과 Refresh Token을 모두 만료시킨다.
Access Token 만료 && Refresh Token 만료 : 에러, 재 로그인하여 둘 다 새로 발급 필요
Access Token 만료 && Refresh Token 유효 : Refresh Token 검증 후 Access Token 재발급
Access Token 유효 && Refresh Token 만료 : Access Token을 검증하여 Refresh Token 재발급
Access Token 유효 && Refresh Token 유효 : 정상
# JWT 사용
gradle dependency 추가
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
Config
Password 인코딩용 Bean 추가
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder encodePassword() {
return new BCryptPasswordEncoder();
}
}
JWT 발급
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
public class JwtTokenUtils {
public static String generateToken(String userName, String key, long expiredTimeMs) {
Claims claims = Jwts.claims();
claims.put("userName", userName);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiredTimeMs))
.signWith(getKey(key), SignatureAlgorithm.HS256)
.compact();
}
private static Key getKey(String key) {
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
- Key 생성
- 토큰 생성 시 Key가 필요
- 알고리즘은 SignatureAlgorithm.HS256 선택
Service
import com.study.toysns.exception.ErrorCode;
import com.study.toysns.exception.SnsApplicationException;
import com.study.toysns.model.User;
import com.study.toysns.model.entity.UserEntity;
import com.study.toysns.repository.UserEntityRepository;
import com.study.toysns.util.JwtTokenUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserEntityRepository userEntityRepository;
private final BCryptPasswordEncoder encoder;
@Value("${jwt.secret-key}")
private String secretKey;
@Value("${jwt.token.expired-time-ms} ")
private Long expiredTimeMs;
public String login(String userName, String password) {
UserEntity userEntity = userEntityRepository.findByUserName(userName)
.orElseThrow(() -> new SnsApplicationException(ErrorCode.USER_NOT_FOUND, String.format("%s not founded", userName)));
if (!encoder.matches(password, userEntity.getPassword())) {
throw new SnsApplicationException(ErrorCode.INVALID_PASSWORD);
}
String token = JwtTokenUtils.generateToken(userName, secretKey, expiredTimeMs);
return token;
}
}
참고
'Develope > ETC' 카테고리의 다른 글
[RESTful] REST API (0) | 2022.08.12 |
---|---|
[Computer Science] URL vs URI (0) | 2022.08.12 |
[Computer Science] HTTP - HyperText Transfer Protocol 이란 (0) | 2022.08.12 |
[객체 지향] SOLID 법칙 - 객체 지향 설계 5대 원리 (0) | 2022.08.12 |
[Computer Science] 운영체제 (0) | 2022.08.09 |