포스트

240307 기록, 다중토큰

LoginFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
  /* 단일토큰 방법
  MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
  String username = myUserDetails.getUsername();

  Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
  Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
  GrantedAuthority auth = iterator.next();

  String role = auth.getAuthority();

  String token = jwtUtil.createJwt(username, role, 60*60*10L);

  response.addHeader("Authorization", "Bearer " + token);
  // 위의 HTTP인증방식은 RFC 7235 정의에 의해서 공식적으로 형식을 지정해줌
  // Authorization: 타입 인증토큰
  // Authorization: Bearer 인증토큰string
  // "Bearer "의 경우 공백을 끝에 붙여줘야 함
  */

  // 유저 정보 가져오기
  String username = authentication.getName();

  Collection<? extends GrantedAuthority> collection = authentication.getAuthorities();
  Iterator<? extends GrantedAuthority> iterator = collection.iterator();
  GrantedAuthority grantedAuthority = iterator.next();

  String role = grantedAuthority.getAuthority();

  String access = jwtUtil.createJwt("access", username, role, 6000000L);
  String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);

  // 응답 설정
  response.setHeader("access", access);
  response.addCookie(createCookie(refresh));
  // createCookie() 메소드 구현
  response.setStatus(HttpStatus.OK.value());
}

private Cookie createCookie(String value){

  Cookie cookie = new Cookie("refresh", value); // 쿠키생성
  cookie.setMaxAge(24*60*60); // 쿠키의 생명주기, refresh 토큰의 생명주기와 동일하게 넣기
// cookie.setSecure(true); // Https 사용시 설정
// cookie.setPath("/"); // 쿠키가 적용될 범위를 설정할 수 있다.
  cookie.setHttpOnly(true); // 프론트에서 자바스크립트로 쿠키에 접근 할 수 없도록 설정

  return cookie;
}
  • 로그인 성공 시 단일 토큰 생성에서 다중토큰 생성으로 로직 변경
  • 토큰 생성을 2개 하는데 각각의 키값으로 access 와 refresh 로 설정
  • ATK의 경우 빈번하게 사용되고, 인증, 인가에 대한 정보를 가지고 있다. 생명주기가 짧다
    • 따라서 빠르게 탈취 가능한 CSRF 공격보다는 보안 로직을 통해 어느정도 방어할 수 있고, 시간이 좀 더 오래 걸리는 XSS 공격을 받겠다는 것
    • ATK는 서버에서 헤더에 발급 후 프론트에서 로컬 스토리지에 저장
  • RTK는 사용 빈도가 상대적으로 적고, 생명주기가 길다. 역할은 토큰의 재발급 뿐이다.
    • RTK는 쿠키에 저장하는데, httpOnly를 통해 XSS 공격에 대한 방어가 가능하다. 하지만 CSRF 공격에 대해 약하다
    • 하지만 RTK의 역할은 토큰의 재발급이기 때문에 CSRF를 통해 경로에 접근하더라도 상대적으로 문제가 될 요소가 적다

JWTUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String createJwt(String category, String username, String role, Long expiredMs){
  Claims claims = Jwts.claims();
  claims.put("category", category);
  claims.put("username", username);
  claims.put("role", role);

  return Jwts.builder()
          .setClaims(claims)
          .setIssuedAt(new Date(System.currentTimeMillis()))
          .setIssuer(issuer)
          .setExpiration(new Date(System.currentTimeMillis() + expiredMs))
          .signWith(key, SignatureAlgorithm.HS256)
          .compact();
}

public String getCategory(String token){
  return Jwts.parserBuilder().setSigningKey(key).build()
          .parseClaimsJws(token).getBody().get("category", String.class);
}
  • JWT를 생성하는 메소드에 category라는 이름으로 ATK와 RTK를 구분할 수 있도록 해줌
  • category의 값으로 access 또는 refresh 가 들어옴
  • 추후 프론트에서 토큰 정보를 보냈을 때
    • 인가가 필요한 경우 access로 category로 보낼 것이고,
    • 유효기간이 끝난 경우 다시 refresh로 category로 보낼 것이기 때문에
  • getCategory() 메소드 정의
트러블 슈팅
  • 현재 로그인을 진행하면 header에 access라는 key로 토큰 값이 넘어오고 cookies에 refresh 값이 넘어옴
  • 로그인 진행 후 특정 인가가 필요한 페이지에 접근 시 403으로 막히고 있음
  • 아직 서버에서 토큰 정보를 받아서 검증하는 로직 구현이 되지 않았기 때문(현재 구현되어 있는 것은 단일 토큰의 검증)
깃 주소

GitHub