서버 기반 인증 시스템 문제점
세션
-유저가 인증할 때 이 기록을 서버에 저장
-메모리에 저장 혹은 데이터베이스 시스템에 저장
-유저의 수(동시접속)가 많은면 서버나 DB에 부하를준다.
확장성
-클러스터링 구성 시 세션 정보도 같이 공유해야함
-서버 구성이 복잡해짐
cors
-쿠키를 여러 도메인에서 관리하는 것이 번거롭다.
토큰기반 인증시스템 작동원리
작동원리
-유저가 아이디와 비밀번호로 로그인 수행
-서버 측에서 해당 정보 검증
-계정 정보가 정확하다면 서버측에서 유저에게 signed 토큰을 발급
-클라이언트에서는 토큰을 저장해 두고 요청마다 토큰을 서버에 함께 전달
-서버에서 토큰을 검증하고 요청에 응답
헤더(Header) - 두 가지 정보를 포함한다.
- typ : 토큰의 타입을 지정 (JWT)
- alg : 해싱 알고리즘 지정 (HMAC SHA256, RSA ..)
정보(payload)
- 토큰에 담을 정보가 포함 (클레임이라고 함, name/value 쌍(key/value)으로 구성)
- 클레임(claim)은 다음 세 분류로 나뉨
* 등록된 (registered) 클레임 - iss, sub, aud, exp, nbf, sat, jti
* 공개 (public) 클레임 - 충돌 방지 이름이 필요 (주로 URI 형식으로 네이밍)
* 비공개 (private) 클레임 - 클라이언트와 서버 간의 협의 하에 사용되는 클레임 이름들
서명 (signature)
- 헤더의 인코딩 값과 정보의 인코딩 값을 합친 후 주어진 비밀키로 해쉬 하여 생성
. 이 두 개 들어가 있는데 header, payload, signature로 구분되어 있는 것으로 보면 된다.
점(.) 제일 앞에 것은 header, 중간 것은 payload, 두 번째 점(.) 뒤에 것은 signature로 구분
토큰을 사용하는구조는 다음과 같습니다.
실습 예제
JAVA
SecurityService.java(interface)
package com.jang.mtg.service;
public interface SecurityService {
String createToken(String subject,long ttlMillis);
String getSubject(String token);
}
SecurityServiceImpl(class)
package com.jang.mtg.service;
import java.security.Key;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Service
public class SecurityServiceImpl implements SecurityService {
private static String secretKey="4C8kum4LxyKWYLM78sKdXrzb8jDCFyfX";
@Override
public String createToken(String subject, long ttlMillis) {
// TODO Auto-generated method stub
System.out.println("들어왔당");
if(ttlMillis ==0) {
throw new RuntimeException("토큰 만료기간은 0이상 이어야합니다.");
}
//HS256방식으로 암호화 방식 설정
System.out.println("여기");
SignatureAlgorithm signatureAlgorithm=SignatureAlgorithm.HS256;
byte[]apiKeySeBytes=DatatypeConverter.parseBase64Binary(secretKey);
Key signingKey=new SecretKeySpec(apiKeySeBytes, signatureAlgorithm.getJcaName());
JwtBuilder builder =Jwts.builder()
.setSubject(subject)
.signWith(signatureAlgorithm, signingKey);
long nowMillis=System.currentTimeMillis();
builder.setExpiration(new Date(nowMillis+ttlMillis));
return builder.compact();
}
//token을 식별
@Override
public String getSubject(String token) {
// TODO Auto-generated method stub
Claims claims=Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
.parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
createtoken 은 HS256방식으로 암호화하여 토큰을 생성하는코드이다.
getSubject는 들어온 토큰값을 이용해서 다시 subject 값을 추출해내는 코드이다.
해당하는 바와 같이 테스트를 해보았다.
Contorller
//토큰 발행
@GetMapping("/token")
public String generateToken(@RequestParam String subject,Model model){
System.out.println(subject);
String token=securityService.createToken(subject, 1000*60*60);
Map<String,Object> map =new HashMap<>();
map.put("userid", subject);
map.put("token", token);
model.addAttribute("user", new User());
System.out.println(map);
return "login";
}
//token 요청(식별)
@GetMapping("security/get/subject")
public String getSubject(@RequestParam String token,Model model) {
String subject =securityService.getSubject(token);
System.out.println(subject);
model.addAttribute("user", new User());
return "login";
}
URL에 http://localhost:8080/token?subject=kim 하는값을 입력을 해보면
{userid=kim, token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJraW0iLCJleHAiOjE2Njg5NDYwNzF9.MGbhb7HFGlzPiOZFNtswp9OZesY1P4AtEsYbN26Tp6s}
userid와 token 값이 나타나는것을 알수있다.
URL에 http://localhost:8080/security/get/subject?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJraW0iLCJleHAiOjE2Njg5NDYwNzF9.MGbhb7HFGlzPiOZFNtswp9OZesY1P4AtEsYbN26Tp6s 해당하는 토큰값을 다시 입력을 해주면 ?
KIM이라는 subject값이 나오는것을 알수있다.
다음과 같은방법으로 다음에는 로그인 및 다른 api를 사용할 때 토큰값을 같이 넘겨 해당하는 사용자가 맞는지
일치시켜보겠다.