포스트

[TIL] Spring Advanced - JWT authentication and role management

JWT authentication flow in Spring, token creation and verification using JwtUtil, Bearer token processing, and permission management with UserRoleEnum

한국어 원문은 여기에서 볼 수 있습니다.
[TIL] Spring Advanced - JWT authentication and role management

What I studied

Handling JWTs

  • Add JWT dependency

  • set jwt.secret.key to application.properties

Creating a JWT Util

Util class
refers to a class that has methods that perform operations on specific parameters (parameters). → A class that operates as a module without depending on other objects

We plan to create a class called JwtUtil with JWT-related functions to perform JWT-related functions.

  • JWT-related features
    1. Create JWT

    2. Save the generated JWT in Cookie

    3. Substring the JWT token contained in the cookie

    4. JWT verification

    5. Get user information from JWT

  • Data required for token creation

    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);
      }
    
    • Write the Secret Key encoded in Base64 in properties and retrieve it through @Value.

    • Encrypt with the Secret Key taken when creating the JWT.
      • At this time, the encoded Secret Key is decoded and used.

      • Key is an object containing the decoded Secret Key.

      • @PostConstruct is used to prevent the mistake of calling a new request every time you use a value that only needs to be received once.

        • Executes after calling the constructor of the JwtUtil class and injects the value into the Key field.
    • The encryption algorithm uses the HS256 algorithm.

    • Bearer indicates that a JWT or OAuth token is used.
📍 The types of user permissions are managed using Enum.
  • Used to enter the user’s permissions using the user’s information when creating a 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";
}
}
Code description
  • UserRoleEnum is a class that manages user permissions (Role) as an enum.

  • By using enum, you can prevent typos by not having to write permission values ​​directly as strings.

  • Currently, two permissions are defined: USER and ADMIN.

1
2
USER(Authority.USER),
ADMIN(Authority.ADMIN);
  • USER is a normal user privilege.

  • ADMIN has administrator privileges.

  • The authority field stores the permission string used by actual Spring Security.

1
private final String authority;
  • Spring Security typically uses permission values ​​with the ROLE_ prefix.
1
2
ROLE_USER
ROLE_ADMIN
  • Store permission strings through the enum constructor.
1
2
3
UserRoleEnum(String authority) {
this.authority = authority;
}
  • The permission string passed when creating an enum is stored in the authority field.
1
2
USER("ROLE_USER")
ADMIN("ROLE_ADMIN")
  • The getAuthority() method returns the stored permission string.
1
2
3
public String getAuthority() {
return this.authority;
}
  • Example
1
UserRoleEnum.USER.getAuthority()
1
ROLE_USER
  • The Authority inner class manages permission string constants.
1
2
3
4
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
  • If you manage the permission string as a constant, you do not need to write the string yourself.

  • Prevent typos and improve maintainability.

1
"ROLE_USRE" // 오타 발생 가능
  • full flow

  • enum name

1
UserRoleEnum.USER

→ Permission types used in code

  • Actual permission string
1
UserRoleEnum.USER.getAuthority()

→ Permission values recognized by Spring Security

1
ROLE_USER
  • Clean up
  • UserRoleEnum
  • Manage user permission types with enums

  • Authority
  • Manage actual permission string constants

  • getAuthority()
  • Returns a permission string to be used in Spring Security

  • Purpose
  • This is to manage permissions safely and consistently.
  1. Create 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();
     }
    
    • Insert the user’s identification value, i.e. ID, into the subject of the JWT.

    • Insert the user’s authorization information into the JWT. You can check it through the key value in key-value format.

    • Enter the token expiration time. It is based on ms.

    • Insert the date of issue into issuedAt.

    • Enter the key containing the secretKey value and the encryption algorithm in signWith.

      • Encrypt JWT using ket and encryption algorithm.
  2. Save to 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());
         }
     }
    
  3. JWT token substring, which is the value of the received cookie

    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");
     }
    
    • Use StringUtils.hasText to check for blank and null, and startsWith to check if the starting value of the token is Bearer.- If correct, truncate the Bearer using substring to return a pure JWT.
  4. JWT verification

    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;
     }
    
    • You can use Jwts.parserBuilder() to parse JWT.

    • Check that the JWT has not been forged or altered by entering the secretKey (key) value.

  5. Get user information from JWT

    1
    2
    3
    4
    
     // 토큰에서 사용자 정보 가져오기
     public Claims getUserInfoFromToken(String token) {
         return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
     }
    
    • The Payload part of the JWT structure contains information contained in the token.

    • A ‘piece’ of information contained here is called a claim (claim), and it consists of a key-value pair. A token can contain multiple claims.

    • Use Jwts.parserBuilder() and secretKey to retrieve JWT Claims and use the user information contained therein.