[TIL] 스프링 숙련 - JWT 인증과 권한 관리
Spring JWT 인증 구현, JwtUtil 토큰 생성 및 검증, Bearer 토큰 처리, UserRoleEnum 권한 관리
For the English version of this post, see here.
공부한 내용
JWT 다루기
JWT dependency 추가하기
application.properties에 jwt.secret.key 설정
JWT Util 만들기
- Util 클래스
- 특정 매개 변수(파라미터)에 대한 작업을 수행하는 메서드들이 존재하는 클래스를 뜻함 → 다른 객체에 의존하지 않고, 하나의 모듈로서 동작하는 클래스
JWT 관련 기능들을 가진 JwtUtil이라는 클래스를 만들어 JWT 관련 기능을 수행시킬 예정
- JWT 관련 기능
JWT 생성
생성된 JWT를 Cookie에 저장
Cookie에 들어있던 JWT 토큰을 Substring
JWT 검증
JWT에서 사용자 정보 가져오기
토큰 생성에 필요한 데이터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Header KEY 값 public static final String AUTHORIZATION_HEADER = "Authorization"; // 사용자 권한 값의 KEY public static final String AUTHORIZATION_KEY = "auth"; // Token 식별자 public static final String BEARER_PREFIX = "Bearer "; // 토큰 만료시간 private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분 @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey private String secretKey; private Key key; private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 로그 설정 public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그"); @PostConstruct public void init() { byte[] bytes = Base64.getDecoder().decode(secretKey); key = Keys.hmacShaKeyFor(bytes); }
Base64로 Encode된 Secret Key를 properties에 작성해두고 @Value를 통해 가져옵니다.
- JWT를 생성할 때 가져온 Secret Key로 암호화합니다.
이때 Encode된 Secret Key를 Decode 해서 사용합니다.
Key는 Decode된 Secret Key를 담는 객체입니다.
@PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용됩니다.
- JwtUtil 클래스의 생성자 호출 이후에 실행되어 Key 필드에 값을 주입 해줍니다.
암호화 알고리즘은 HS256 알고리즘을 사용합니다.
- Bearer 란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시입니다.
- 로깅이란 애플리케이션이 동작하는 동안 프로젝트의 상태나 동작 정보를 시간순으로 기록하는 것을 의미합니다.
- 우리는 Logback 로깅 프레임워크를 사용해서 로깅을 진행하도록 하겠습니다.
- JWT를 생성할 때 사용자의 정보로 해당 사용자의 권한을 넣어줄 때 사용함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
코드 설명
UserRoleEnum은 사용자 권한(Role)을 enum으로 관리하는 클래스입니다.enum을 사용하면 권한 값을 문자열로 직접 작성하지 않아도 되어 오타를 방지할 수 있습니다.
현재 권한은
USER,ADMIN두 가지로 정의되어 있습니다.
1
2
USER(Authority.USER),
ADMIN(Authority.ADMIN);
USER는 일반 사용자 권한입니다.ADMIN은 관리자 권한입니다.authority필드는 실제 Spring Security에서 사용하는 권한 문자열을 저장합니다.
1
private final String authority;
- Spring Security는 일반적으로
ROLE_prefix가 포함된 권한 값을 사용합니다.
1
2
ROLE_USER
ROLE_ADMIN
- enum 생성자를 통해 권한 문자열을 저장합니다.
1
2
3
UserRoleEnum(String authority) {
this.authority = authority;
}
- enum 생성 시 전달된 권한 문자열이
authority필드에 저장됩니다.
1
2
USER("ROLE_USER")
ADMIN("ROLE_ADMIN")
getAuthority()메서드는 저장된 권한 문자열을 반환합니다.
1
2
3
public String getAuthority() {
return this.authority;
}
- 예시
1
UserRoleEnum.USER.getAuthority()
1
ROLE_USER
Authority내부 클래스는 권한 문자열 상수를 관리합니다.
1
2
3
4
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
권한 문자열을 상수로 관리하면 문자열을 직접 작성하지 않아도 됩니다.
오타를 방지하고 유지보수성을 높일 수 있습니다.
1
"ROLE_USRE" // 오타 발생 가능
전체 흐름
enum 이름
1
UserRoleEnum.USER
→ 코드에서 사용하는 권한 타입
- 실제 권한 문자열
1
UserRoleEnum.USER.getAuthority()
→ Spring Security가 인식하는 권한 값
1
ROLE_USER
- 정리
UserRoleEnum사용자 권한 종류를 enum으로 관리
Authority실제 권한 문자열 상수 관리
getAuthority()Spring Security에서 사용할 권한 문자열 반환
- 목적
- 권한을 안전하고 일관성 있게 관리하기 위함입니다.
JWT 생성
1 2 3 4 5 6 7 8 9 10 11 12 13
// 토큰 생성 public String createToken(String username, UserRoleEnum role) { Date date = new Date(); return BEARER_PREFIX + Jwts.builder() .setSubject(username) // 사용자 식별자값(ID) .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 .setIssuedAt(date) // 발급일 .signWith(key, signatureAlgorithm) // 암호화 알고리즘 .compact(); }
JWT의 subject에 사용자의 식별값 즉, ID를 넣습니다.
JWT에 사용자의 권한 정보를 넣습니다. key-value 형식으로 key 값을 통해 확인할 수 있습니다.
토큰 만료시간을 넣습니다. ms 기준입니다.
issuedAt에 발급일을 넣습니다.
signWith에 secretKey 값을 담고있는 key와 암호화 알고리즘을 값을 넣어줍니다.
- ket와 암호화 알고리즘을 사용하여 JWT를 암호화합니다.
JWT Cookie에 저장
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// JWT Cookie 에 저장 public void addJwtToCookie(String token, HttpServletResponse res) { try { token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행 Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value cookie.setPath("/"); // Response 객체에 Cookie 추가 res.addCookie(cookie); } catch (UnsupportedEncodingException e) { logger.error(e.getMessage()); } }
받아온 Cookie의 Value인 JWT 토큰 substring
1 2 3 4 5 6 7 8
// JWT 토큰 substring public String substringToken(String tokenValue) { if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { return tokenValue.substring(7); } logger.error("Not Found Token"); throw new NullPointerException("Not Found Token"); }
StringUtils.hasText를 사용하여 공백, null을 확인하고 startsWith을 사용하여 토큰의 시작값이 Bearer이 맞는지 확인합니다.
맞다면 순수 JWT를 반환하기 위해 substring을 사용하여 Bearer을 잘라냅니다.
JWT 검증
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 토큰 검증 public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (SecurityException | MalformedJwtException | SignatureException e) { logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); } catch (ExpiredJwtException e) { logger.error("Expired JWT token, 만료된 JWT token 입니다."); } catch (UnsupportedJwtException e) { logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); } catch (IllegalArgumentException e) { logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); } return false; }
Jwts.parserBuilder()를 사용하여 JWT를 파싱할 수 있습니다.JWT가 위변조되지 않았는지 secretKey(key)값을 넣어 확인합니다.
JWT에서 사용자 정보 가져오기
1 2 3 4
// 토큰에서 사용자 정보 가져오기 public Claims getUserInfoFromToken(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); }
JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있습니다.
여기에 담긴 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 key-value 의 한 쌍으로 이뤄져있습니다. 토큰에는 여러개의 클레임 들을 넣을 수 있습니다.
Jwts.parserBuilder()와 secretKey를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자의 정보를 사용합니다.
댓글
궁금한 점, 피드백, 오류 제보를 남겨 주세요.