본문 바로가기
개발

JWT 활용한 로그인 구현 - 2

by 공덕뉸나 2023. 8. 31.

지난 글에서는 로그인 기능을 사용하기 위해 Users 엔티티를 추가, 수정했다.

이번엔 JWT Token 방식을 위한 클래스들을 정리해보고자 한다.

 

JWT란?

Json Web Token의 줄임말로 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다.

 

JWT 구조

- Header

토큰의 타입을 지정하는 type과 알고리즘 방식을 지정하며, 서명 및 토큰 검증에 사용되는 alg로 구성된다.

- PayLoad

토큰에서 사용할 정보의 조각들인 Claim이 담겨있다. 

- Signature

토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.

 

JwtUtil.java

Jwt Token 방식을 사용할 때 필요한 기능들을 정리해놓은 클래스이다.

 

public class JwtUtil {

    private static String secretKey;
    private static String refreshKey;
    private static byte[] secretBytes;
    private static byte[] refreshBytes;

    public JwtUtil(String secretKey, String refreshKey) throws UnsupportedEncodingException {
        this.secretKey = secretKey;
        this.refreshKey = refreshKey;
        this.secretBytes = secretKey.getBytes("UTF-8");
        this.refreshBytes = refreshKey.getBytes("UTF-8");
    }

    public static String generateToken(Map<String, Object> claims) {
        String token = Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, secretBytes)
            .compact();

        return token;
    }

    public static String generateRefreshToken(Map<String, Object> claims) {
        String token = Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, refreshBytes)
            .compact();

        return token;
    }

    public static Map<String, Object> getClaims(String jwt) {
        if (jwt == null || jwt.isEmpty()) {
            return null;
        }

        Claims claims = Jwts.parser()
            .setSigningKey(secretBytes)
            .parseClaimsJws(jwt)
            .getBody();
        return claims;
    }

    public static Map<String, Object> getClaimsForReFresh(String jwt) {
        if (jwt == null || jwt.isEmpty()) {
            return null;
        }

        Claims claims = Jwts.parser()
            .setSigningKey(refreshBytes)
            .parseClaimsJws(jwt)
            .getBody();
        return claims;
    }
}

 

TokenService.java

generateDefaultClaims에서 Claim에 user 정보나 토큰 정보들을 넣어준다.

iss : 토큰 발급자 

sub : 토큰 제목

exp : 토큰 만료 시간

iat : 토큰 발급 시간

 

getUserDetailsByToken 

token을 통해 user 정보를 꺼낼 수 있다.

 

@Service
@RequiredArgsConstructor
public class TokenService {

    private static Long defaultExpirationMinutes;

    @Value("${util.jwt.defaultExpirationMinutes}")
    public void setDefaultExpirationMinutes(Long defaultExpirationMinutes) {
        TokenService.defaultExpirationMinutes = defaultExpirationMinutes;
    }

    public String parseTokenByRequest(HttpServletRequest request) {
        final String authHeader = request.getHeader("authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }

    public static Map<String, Object> generateDefaultClaims(Users user, Long plusExpMinutes) {
        LocalDateTime now = LocalDateTime.now();

        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", "pilot-access-token");
        claims.put("iss", "pilot");
        claims.put("iat", Timestamp.valueOf(now));
        claims.put("exp", Timestamp.valueOf(now.plusMinutes(plusExpMinutes)).getTime() / 1000);
        claims.put("userId", user.getUserId());
        claims.put("userName", user.getUserName());
        claims.put("userEmail", user.getUserEmail());
        claims.put("created",
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));

        return claims;
    }

    public static Map<String, Object> generateReFreshClaims(Users user, Long plusExpMinutes) {
        LocalDateTime now = LocalDateTime.now();

        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", "pilot-refresh-token");
        claims.put("iss", "pilot");
        claims.put("userId", user.getUserId());
        claims.put("iat", Timestamp.valueOf(now));
        claims.put("exp", Timestamp.valueOf(now.plusMinutes(plusExpMinutes)).getTime() / 1000);
        claims.put("created",
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));

        return claims;
    }

    public static String generateToken(Users user, Boolean remember) {
        Long expMin = remember ? defaultExpirationMinutes * 24 * 7 : defaultExpirationMinutes;
        Map<String, Object> claims = generateDefaultClaims(user, expMin);

        return JwtUtil.generateToken(claims);
    }

    public static String generateReFreshToken(Users user, Boolean remember) {
        Long expMin = remember ? defaultExpirationMinutes * 24 * 30 : defaultExpirationMinutes * 4;
        Map<String, Object> claims = generateReFreshClaims(user, expMin);

        return JwtUtil.generateRefreshToken(claims);
    }

    public CustomUserDetails getUserDetailsByToken(String token) {
        Map<String, Object> claims = JwtUtil.getClaims(token);
        if (claims == null) {
            throw new NotAuthorizedException("Invalid token (" + token + ")");
        }

        Long userId = Long.parseLong(claims.get("userId").toString());
        String userEmail = claims.get("userEmail").toString();
        String userName = claims.get("userName").toString();
        String phoneNumber = (String) claims.get("phoneNumber");
        List<String> userRoles = null;
        if (claims.containsKey("userRoles")) {
            userRoles = (List<String>) claims.get("userRoles");
        }

        return new CustomUserDetails(userId, userRoles, userEmail, userName, phoneNumber);
    }

}

 

WebSecurityConfig.java

Spring Security에 대한 설정이다.

 

- SessionCreationPolicy.STATELESS

토큰 로그인 방식에서는 session이 필요하지 않다.

- addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)

로그인을 진행해주는 필터에 가기 전에 TokenFilter를 거치게 한다.

TokenFilter에서 사용자의 요청에서 Jwt Token을 추출한 후 해당 Token이 유효한지를 체크하고 유효하다면 UsernamePasswordAuthenticationFilter를 통화할 수 있도록 하는 것이다.

 

 

버전이 바뀌면서 기존 사용 방식이 deprecated 되었다.

 

기존 코드 예시 

   @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .and().build();
    }

httpBasic().disable(), csft().disable() 이런 형태로 쓰였었다.

 

바뀐 코드

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final TokenFilter tokenFilter;
    private final ExceptionHandlerFilter exceptionHandlerFilter;
    private final EncodingFilter encodingFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(encodingFilter, CsrfFilter.class)
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(exceptionHandlerFilter, TokenFilter.class)
                .authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll())
                .build();
    }
}

 

SecurityService.java

user 정보로 UsernamePasswordAuthenticationToken 발급 후 권한 부여한다.

 

@Service
@RequiredArgsConstructor
public class SecurityService {

    public void setAuthentication(CustomUserDetails userDetails) {
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken(
                userDetails
                , null
                , userDetails.getAuthorities()));
    }
}

 

TokenFilter.java

jwt가 유효한 토큰인지 인증하기 위한 필터이다.

@Component
@RequiredArgsConstructor
public class TokenFilter extends OncePerRequestFilter {

    private final TokenService tokenService;
    private final SecurityService securityService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        try {
            final String token = tokenService.parseTokenByRequest(request);
            if (token != null) {
                CustomUserDetails userDetails = tokenService.getUserDetailsByToken(token);
                securityService.setAuthentication(userDetails);
            }
        } catch (ExpiredJwtException | NotAuthorizedException e) {
            throw new TokenFilterException(e);
        } catch (Exception e) {
            throw new NotAuthorizedException("Invalid Token.", e);
        }

        filterChain.doFilter(request, response);
    }
}

 

여기까지 하면 로그인 기능을 구현하기 위한 준비가 다 되었다.

exception 같은 경우는 내가 사용하고자 하는 것을 만들면 되기 때문에 글에 적지는 않았다.

JWT 사용 방법을 다시 익히기 위해 학습하며 작성했는데 이해하기 좀 헷갈리긴 했다..

부족한 부분이 있겠지만 개발하면서 더 학습해야겠다.

다음에는 로그인 API 만드는 것으로 또 포스팅 해야겠다. 망망~

 

'개발' 카테고리의 다른 글

Querydsl 세팅하기  (0) 2023.09.06
JWT 활용한 로그인 구현 - 3  (0) 2023.09.01
JWT 활용한 로그인 구현 - 1  (0) 2023.08.30
Enum 생성  (1) 2023.08.23
BaseEntity 생성하기  (0) 2023.08.19