Spring, Apache, Java

Spring Security + JWT로 회원 인증, 인가 구현하기

cwchoiit 2024. 4. 30. 14:13
728x90
반응형
SMALL

우선, 이 페이지가 세션 기반 인증 방식이 아닌 이유를 먼저 설명해야 할 것 같다.

왜 JWT를 사용했나?

모바일 앱의 등장이 주 원인이라고 할 수 있다. 모바일 앱을 추후에 만든다고 해도 통합된 인증 체계로부터 얻는 이점이 분명히 있다고 생각했다.

 

모바일 앱을 사용할 땐 주로 토큰 기반 인증 방식을 사용한다. 그 이유는 다음과 같다.

  • 장기 세션 유지의 어려움: 모바일 디바이스는 종종 네트워크 연결 상태가 변할 수 있고, 앱이 백그라운드에서 종료되거나 장치가 재부팅될 수 있다. 세션은 이런 환경에서는 연결이 끊기기 쉬워 토큰 방식이 더 안정적인 경우가 많다.
  • 스케일러빌리티: 세션 정보를 서버에서 관리해야 할 경우, 사용자가 많아질수록 서버의 부담이 커질 수 있다. 반면 토큰은 클라이언트 측에서 관리되기 때문에 서버는 인증을 확인만 하면 되므로 부하가 줄어든다.
  • 다양한 플랫폼 지원: 모바일 앱뿐만 아니라 웹사이트, 다른 종류의 클라이언트에서도 동일한 방식으로 인증 시스템을 구현할 수 있다. 이는 통합된 인증 체계를 유지하기에 유리하다.

이러한 이유들이 내가 인증 방식을 JWT로 구현하고 싶어지게 했다. 

우선, 개발 환경은 다음과 같다.

  • Spring Boot 3.2.2
  • Java 21
  • Spring Security 3.2.2
  • io.jsonwebtoken:jjwt 0.12.3
  • MySQL 8

로그인 흐름

유저가 /login 으로 username, password를 같이 보내면 스프링 시큐리티 내부적으로는 UsernamePasswordAuthenticationFilter라는 필터를 통해 해당 유저가 현재 Database에 존재하는 유저인지 찾고 그 유저의 로그인 정보(ID, PW)가 일치하는지 확인해서 맞다면 JWT토큰을 만들어서 유저에게 반환한다. (그림에서는 JWT 토큰 반환은 생략)

 

인증과 인가의 차이가 뭔가요?
  • 인증(Authentication): 인증은 사용자가 누구인지 확인하는 과정. 사용자가 시스템에 로그인할 때 아이디와 비밀번호를 제공하는 것이 대표적인 예. 스프링 시큐리티에서는 AuthenticationManager가 이 역할을 담당하고, 사용자의 신원을 확인한 후 'Authentication' 객체에 이 정보를 저장한다.
  • 인가(Authorization): 인가는 인증된 사용자가 특정 자원에 접근하거나 특정 작업을 수행할 수 있는 권한을 가지고 있는지 확인하는 과정. 예를 들어 어떤 사용자가 특정 페이지에 접근하거나 데이터를 수정할 권한이 있는지 검사하는 것

 

Unauthorization과 Forbidden의 차이는요?
  • Unauthorization(401): 인증이 실패했을 때 반환. 아이디나 비밀번호가 잘못됐거나 아예 제공되지 않았을 때. 즉, 사용자가 누구인지 시스템이 식별하지 못했을 때 발생
  • Forbidden(403): 사용자 인증은 성공적으로 마쳤지만, 요청한 자원에 대한 접근 권한이 없을 때 반환. 예를 들어, 사용자가 로그인은 했지만 관리자 페이지에 접근하려 할 때 해당 페이지에 대한 접근 권한이 없는 경우 발생.

정리하자면, "Unauthorization은 당신이 누구인지 모르겠으니 로그인이 필요합니다"라는 의미이고, Forbidden은 "당신이 누구인지 알겠지만, 이 작업을 수행할 권한이 없습니다"라는 의미이다.

 

 

의존성

build.gradle

//Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

//JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

//MySQL
implementation 'mysql:mysql-connector-java:8.0.33'

엔티티 설계 및 구현과 레포지토리

내가 만드는 서비스에 접속할 수 있는 유저는 오로지 어드민뿐이다. 그 중에서도 선택받은 어드민 유저만 접속하게 하기 위해 Administrator라는 엔티티를 만들었다.

 

Administrator (src/main/java/path/your/package/entity/Administrator.java)

import jakarta.persistence.*;
import kr.co.tbell.mm.entity.BaseEntity;
import lombok.*;

@Getter
@Entity
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Administrator extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;
    private String password;

    @Enumerated(value = EnumType.STRING)
    private Role role;

    @Column(nullable = false)
    private boolean isExpired;
    @Column(nullable = false)
    private boolean isLocked;
    @Column(nullable = false)
    private boolean isCredentialsExpired;

    @Builder.Default
    @Column(nullable = false)
    private boolean isEnabled = true;

    public boolean isAccountNonExpired() {
        return !isExpired;
    }

    public boolean isAccountNonLocked() {
        return !isLocked;
    }

    public boolean isCredentialsNonExpired() {
        return !isCredentialsExpired;
    }

    public boolean isEnabled() {
        return isEnabled;
    }
}

 

Role Enum 클래스를 보자. 이 Enum 클래스에선 Role이 추가될수도 아닐수도 있겠지만 스프링 시큐리티에서 롤 관련 인가 정책을 잘 만들어 두었기 때문에 사용해보기로 했다.

 

Role (src/main/java/path/your/package/entity/Role.java)

import lombok.Getter;

import java.util.Arrays;

@Getter
public enum Role {
    ROLE_ADMIN("ROLE_ADMIN");

    private final String description;

    Role(String description) {
        this.description = description;
    }

    public static Role getRole(String description) {
        return Arrays.stream(Role.values())
                .filter(role -> role.description.equals(description))
                .findFirst()
                .orElse(null);
    }
}

 

AdministratorRepository (src/main/java/path/your/package/repository/AdministratorRepository.java)

레포지토리는 간단하다. 추후에 구현해야 할 UserDetailsService 인터페이스를 위해 findByUsername()을 인터페이스에 추가했다.

import kr.co.tbell.mm.entity.administrator.Administrator;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface AdministratorRepository extends JpaRepository<Administrator, Long> {
    Optional<Administrator> findByUsername(String username);
}

 

회원가입

회원가입을 해야 로그인을 할 수 있으니까 회원가입 컨트롤러 - 서비스 레벨을 구현한다.

회원가입 컨트롤러

AdministratorController (src/main/java/path/your/package/controller/AdministratorController.java)

import jakarta.validation.Valid;
import kr.co.tbell.mm.dto.administrator.CustomAdministratorDetails;
import kr.co.tbell.mm.dto.administrator.ReqCreateAdministrator;
import kr.co.tbell.mm.dto.administrator.ResCreateAdministrator;
import kr.co.tbell.mm.dto.common.Response;
import kr.co.tbell.mm.entity.administrator.Administrator;
import kr.co.tbell.mm.entity.administrator.Role;
import kr.co.tbell.mm.service.administrator.AdministratorService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import javax.management.InstanceAlreadyExistsException;

@Slf4j
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
public class AdministratorController {

    private final AdministratorService administratorService;

    @PostMapping("/signup")
    public ResponseEntity<Response<ResCreateAdministrator>> signup(
            @RequestBody @Valid ReqCreateAdministrator reqCreateAdministrator) {
        ResCreateAdministrator administrator;

        try {
            administrator = administratorService.createAdministrator(reqCreateAdministrator);
        } catch (InstanceAlreadyExistsException e) {
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(new Response<>(false, e.getMessage(), null));
        }

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(new Response<>(true, null, administrator));
    }
}

 

위 컨트롤러에서 사용하는 DTO정보는 다음과 같다.

 

ReqCreateAdministrator (src/main/java/path/your/package/dto/ReqCreateAdministrator.java)

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ReqCreateAdministrator {
    @NotNull(message = "'username' must be required.")
    private String username;
    @NotNull(message = "'password' must be required.")
    private String password;
}

 

ResCreateAdministrator (src/main/java/path/your/package/dto/ResCreateAdministrator.java)

import kr.co.tbell.mm.entity.administrator.Role;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ResCreateAdministrator {
    private Long id;
    private String username;
    private Role role;
}

회원가입 서비스 

AdministratorService (src/main/java/path/your/package/service/AdministratorService.java)

import kr.co.tbell.mm.dto.administrator.ReqCreateAdministrator;
import kr.co.tbell.mm.dto.administrator.ResCreateAdministrator;

import javax.management.InstanceAlreadyExistsException;

public interface AdministratorService {
    ResCreateAdministrator createAdministrator(ReqCreateAdministrator reqCreateAdministrator)
            throws InstanceAlreadyExistsException;
}

 

AdministratorServiceImpl (src/main/java/path/your/package/service/AdministratorServiceImpl.java)

import kr.co.tbell.mm.dto.administrator.*;
import kr.co.tbell.mm.entity.administrator.Administrator;
import kr.co.tbell.mm.entity.administrator.Role;
import kr.co.tbell.mm.repository.administrator.AdministratorRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.management.InstanceAlreadyExistsException;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class AdministratorServiceImpl implements AdministratorService, UserDetailsService {

    private final AdministratorRepository administratorRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public ResCreateAdministrator createAdministrator(ReqCreateAdministrator reqCreateAdministrator)
            throws InstanceAlreadyExistsException {
        Optional<Administrator> adminOptional =
                administratorRepository.findByUsername(reqCreateAdministrator.getUsername());

        if (adminOptional.isPresent()) {
            throw new InstanceAlreadyExistsException("Admin already exists with username: "
                    + reqCreateAdministrator.getUsername());
        }

        Administrator admin = Administrator
                .builder()
                .username(reqCreateAdministrator.getUsername())
                .password(passwordEncoder.encode(reqCreateAdministrator.getPassword()))
                .role(Role.ROLE_ADMIN)
                .build();

        Administrator savedAdmin = administratorRepository.save(admin);

        return new ResCreateAdministrator(savedAdmin.getId(), savedAdmin.getUsername(), savedAdmin.getRole());
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Administrator> byUsername = administratorRepository.findByUsername(username);
        if (byUsername.isEmpty()) {
            throw new UsernameNotFoundException(username);
        }

        return new CustomAdministratorDetails(byUsername.get());
    }
}

AdministratorServiceImpl에서는 두개의 인터페이스를 구현한다. AdministratorSerivce, UserDetailsService.

UserDetailsService는 스프링 시큐리티에서 회원 인증을 위해 구현해야 하는 인터페이스다. 이 인터페이스를 구현해서 데이터베이스로부터 특정 'Username'을 가진 유저가 있는지 찾아, 있다면 UserDetails 라는 인터페이스 타입의 클래스를 반환한다. UserDetails도 마찬가지로 직접 구현해야한다.

 

CustomAdministratorDetails (src/main/java/path/your/package/dto/CustomAdministratorDetails.java)

이 클래스가 UserDetails를 구현하는 클래스이다.

import kr.co.tbell.mm.entity.administrator.Administrator;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@RequiredArgsConstructor
public class CustomAdministratorDetails implements UserDetails {

    private final Administrator administrator;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return administrator.getRole().getDescription();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {
        return administrator.getPassword();
    }

    @Override
    public String getUsername() {
        return administrator.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return administrator.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return administrator.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return administrator.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return administrator.isEnabled();
    }
}

 

SecurityConfiguration

SecurityConfiguration (src/main/java/path/your/package/config/SecurityConfiguration.java)

import jakarta.servlet.http.HttpServletRequest;
import kr.co.tbell.mm.entity.administrator.Role;
import kr.co.tbell.mm.jwt.JwtFilter;
import kr.co.tbell.mm.jwt.JwtManager;
import kr.co.tbell.mm.jwt.LoginFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtManager jwtManager;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // JWT 방식을 사용하기 때문에 CSRF 공격에 대한 위험성 X
        http.csrf(AbstractHttpConfigurer::disable);

        // JWT 방식을 사용하기 때문에 FormLogin, Basic 방식을 Disable
        http.formLogin(AbstractHttpConfigurer::disable);
        http.httpBasic(AbstractHttpConfigurer::disable);

        // 한개의 아이디에 대해 최대 중복 로그인 개수 maximumSessions
        // maxSessionPreventsLogin 다중 로그인 개수를 초과했을 때 처리방법. true: 새로운 로그인 차단, false: 기존 세션 하나 삭제
        // JWT 방식을 사용하기 때문에 SessionCreationPolicy STATELESS
        http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // AntPathRequestMatcher 객체를 사용한 이유는 개인적으로 가시성이 좀 더 좋고 어떤 Http Method인지 바로 알 수 있어서 사용
        // 굳이 안 사용해도 된다.
        http.authorizeHttpRequests(request ->
                request.requestMatchers(
                        new AntPathRequestMatcher("/api/v1/admin/signup", "POST"),
                        new AntPathRequestMatcher("/login", "POST")
                        ).permitAll()
                        .anyRequest().hasRole("ADMIN"));

        // LoginFilter 앞에 JwtFilter 등록
        http.addFilterBefore(new JwtFilter(jwtManager), LoginFilter.class);

        // addFilterAt은 정확히 그 필터(UsernamePasswordAuthenticationFilter)를 내가 만든 LoginFilter로 대체하겠다는 메서드.
        // addFilterBefore, addFilterAfter 이 것들은 말 그대로 그 전 또는 그 후에 붙이겠다는 의미
        http.addFilterAt(
                new LoginFilter(authenticationManager(authenticationConfiguration), jwtManager),
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

위 코드에서 URL Path에 따라 인증이 필요한지 또는 특정 Role을 가진 유저인지 확인하는 다음 코드를 보자.

http.authorizeHttpRequests(request ->
    request.requestMatchers(
            new AntPathRequestMatcher("/api/v1/admin/signup", "POST"),
            new AntPathRequestMatcher("/login", "POST")
            ).permitAll()
            .anyRequest().hasRole("ADMIN"));

이 코드는 "/api/v1/admin/signup", "/login"으로의 요청은 인증이 필요없이 모두 허가한다는 의미이고, 그 외(anyRequest())는 모두 ADMIN Role이 있어야 접근 가능하다는 설정이다.

 

 

LoginFilter (src/main/java/path/your/package/jwt/LoginFilter.java)

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.co.tbell.mm.dto.administrator.CustomAdministratorDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtManager jwtManager;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        log.info("[attemptAuthentication]: Username : {}, Password: {}", username, password);

        UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(username, password, null);

        return authenticationManager.authenticate(authToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authentication) {
        log.info("[successfulAuthentication]: Authentication Success");
        CustomAdministratorDetails administratorDetails = (CustomAdministratorDetails) authentication.getPrincipal();

        String username = administratorDetails.getUsername();
        String role = authentication
                        .getAuthorities()
                        .iterator()
                        .next()
                        .getAuthority();

        String token = jwtManager.createJwt(username, role);

        response.addHeader("Authorization", "Bearer " + token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException failed) throws IOException {
        log.info("[unsuccessfulAuthentication]: Authentication Failed");

        String errorMessage = String.format("error: Failed login with this username: %s", obtainUsername(request));

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write(errorMessage);
    }
}

 

JwtFilter (src/main/java/path/your/package/jwt/JwtFilter.java)

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.co.tbell.mm.dto.administrator.CustomAdministratorDetails;
import kr.co.tbell.mm.entity.administrator.Administrator;
import kr.co.tbell.mm.entity.administrator.Role;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtManager jwtManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

        if (authorization == null || !authorization.startsWith("Bearer ")) {
            log.error("[doFilterInternal]: JWT token does not exist.");
            filterChain.doFilter(request, response);
            return;
        }

        String token = authorization.split(" ")[1];
        if (jwtManager.isExpired(token)) {
            log.error("[doFilterInternal]: JWT token expired.");
            filterChain.doFilter(request, response);
            return;
        }

        String username = jwtManager.getUsername(token);
        String roleStringValue = jwtManager.getRole(token);

        // 현재 로그인 한 사용자 정보를 기반으로 Administrator 객체 생성
        Administrator administrator = Administrator
                .builder()
                .username(username)
                .role(Role.getRole(roleStringValue))
                .build();

        // UserDetails 회원 정보 객체에 현재 로그인 한 사용자 담기
        CustomAdministratorDetails administratorDetails = new CustomAdministratorDetails(administrator);

        // 스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(
                administratorDetails,
                null,
                administratorDetails.getAuthorities());

        // 스프링 시큐리티 세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}

 

JwtManager (src/main/java/path/your/package/jwt/JwtManager.java)

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtManager {

    private final SecretKey secretKey;

    public JwtManager(@Value("${jwt.secret}") String secret) {
        secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    private static final long EXPIRED_MS = 1800000L; // 30분

    public String getUsername(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .get("username", String.class);
    }

    public String getRole(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .get("role", String.class);
    }

    public Boolean isExpired(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .getExpiration()
                .before(new Date());
    }

    public String createJwt(String username, String role) {
        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis()  + EXPIRED_MS))
                .signWith(secretKey)
                .compact();
    }
}

 

 

테스트

Authorization을 헤더에 넣지 않고 요청하면 특정 Path가 아니고선 전부 403 Forbidden.

 

 

로그인

/login으로 username, password를 입력 후 요청하면 200 OK가 떨어지고 Response HeadersJWT Token이 발급된다.

 

Token을 이용해서 다시 위에서 인가 거부된 요청을 날려보면 다음과 같이 정상 응답한다.

 

728x90
반응형
LIST

'Spring, Apache, Java' 카테고리의 다른 글

AOP (Part. 3) - 포인트컷  (0) 2024.01.02
AOP (Part.2)  (0) 2024.01.02
AOP(Aspect Oriented Programming)  (0) 2023.12.29
AOP와 @Aspect, @Around  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27