스프링 시큐리티 JWT

장수빈·2026년 7월 22일

출처는 '스프링 시큐리티 JWT(개발자 유미)'

1. JWT 시큐리티 동작 원리

1) 로그인(인증)


로그인 요청을 받은 후 세션 방식은 서버 세션이 유저 정보를 저장하지만 JWT 방식은 토큰을 생성하여 응답한다.

저번에 Spring Security를 활용하여 프로그램을 만들때에는 formLogin을 사용했기 때문에 세션 로그인 방식을 활용하였다.
하지만 이번 프로젝트에서는 JWT 방식을 활용하여 로그인을 구현해보겠다.

2) 경로 접근(인가)

경로 접근요청이 들어왔을 경우, JWT Filter를 통해 요청의 헤더에서 JWT를 찾아 검증한 후, 일시적 요청에 대한 세션을 생성한다. (생성된 세션은 요청이 끝나면 소멸)

JWT 경로 접근과 FormLogin 경로 접근

Form Login
JSESSIONID → 세션에서 Authentication 복원 → 인가 검사
JWT
JWT 검증 → Authentication을 매 요청마다 생성 → 인가 검사

2. 기본 세팅

스프링 이니셜라이저 -> 파일 실행

의존성 주석처리(이니셜라이저에서 추가하고 연결하지 않으면 런타임 에러 발생)

1) JWT 필수 의존성 세팅

JWT 토큰을 관리하기 위해 의존성을 필수로 추가해주어야 한다.
JWT 버전은 강사님에 따라 0.12.3 버전을 사용한다.

build.gradle dependencies에 JWT 토큰 관리 코드를 넣길래 이것도 스프링 이니셜라이저에서 다운받을 수 있었던거 아닌가 했는데 GTP가 이거는 스프링 이니셜라이저에서 따로 다운받을 수 있었던게 아니라 이렇게 버전확인하고 복사해오는 게 맞다고 가르쳐주었다

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

2) Controller 작성

main controller

@Controller
@ResponseBody
public class MainController {

    @GetMapping("/")
    public String mainP(){

        return "main controller";
    }
}

mainP로 이동하게 하기 위해서 @RestController가 아닌 @Controller 어노테이션을 사용해서 main controller라는 html 파일을 찾도록 설정하였다.

라고 생각했는데 @Controller + @ResponseBody를 같이 쓰면 기능적으로 @RestController를 쓰는 것이랑 거의 동일하다고 한다.

@Controller + @ResponseBody

@RestController와 기능적으로 거의 동일
html을 찾지않고 문자열을 그대로 반환한다.

3) Security Config(설정)

어노테이션

@Configuration: Config 클래스라는 것을 알림으로서 @Bean으로 등록된 객체들을 Spring이 관리할 수 있도록 한다.
@EnableWebSecurity: SpringSecurity의 웹 보안 기능을 활성화하고 설정할 수 있도록 한다.
-> 이 클래스가 스프링 설정 클래스이며 웹 보안 설정을 담당하는 클래스라는 의미가 된다.

그런데 요즘 업데이트된 버전은 @EnableWebSecurity가 없어도 @Bean을 통해 등록해주면 Security 기능을 쓸 수 있다고 한다.
(명시적으로 하기 위해 여전히 쓰기도 함)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 비밀번호 암호화
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        // csrf disable
        http
                .csrf((auth) -> auth.disable());

        // form 로그인 방식 disable
        http
                .formLogin((auth) -> auth.disable());

        // http Basic 인증 방식 disable
        http
                .httpBasic((auth) -> auth.disable());


        // 경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated());


        // 세션 설정
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    return http.build();
    }
}

첫번째 @Bean 비밀번호 검증 및 암호화, 두번째 @Bean JWT 로그인 폼을 만들기 위한 설정변경

JWT를 사용하기 위해서 Spring Security에서 기본적으로 제공하는 form 로그인 방식을 비활성화하고

JWT를 사용하기 때문에 요청마다 아이디와 비밀번호를 헤더에 담는 인증 방식 http basic 기능을 disable했다.

또한 저번 Spring Security 7 수업을 들었을 때는 session을 state, 즉 기본으로 두어 따로 세션 설정을 만들지 않았는데 이번 JWT 로그인은 요청을 보낼 때마다 토큰을 따로 보내므로 로그인 정보를 저장하는 세션 설정을 stateless로 변경하였다.

4) DB 연결 및 ORM

ORM(Object-Relational Mapping)

자바 객체와 관계형 DB 테이블을 서로 연결해 주는 기술
-> 개발자가 SQL 코드를 길게 작성하지 않아도 데이터베이스 테이블과 객체를 연결해준다.

DB는 앞서 했던 것과 동일한 방법으로 연결하였다.

Hibernate ddl 설정 : application.properites

spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

JPA와 hibernate가 데이터베이스를 어떻게 다룰지 정하는 코드

ddl-auto: none

create → 실행할 때마다 기존 테이블을 지우고 새로 생성
create-drop → 실행 시 생성, 종료 시 삭제
update → 엔티티 구조에 맞게 테이블 수정
validate → 엔티티와 테이블 구조가 맞는지만 검사
none → 아무 작업도 하지 않음
.
첫 실행만 none을 create로 바꾸어 실행하고 그 뒤엔 다시 none으로 바꾼다.
첫 실행에서 테이블을 생성했는데 계속 create로 둘 경우 테이블을 계속 지우고 새로 생성하기 때문 -> 주의해서 다루어야함

이렇게 테이블이 잘 만들어진 것을 확인할 수 있었다.

또한 밑의 설정은 자바 클래스, 필드 이름을 DB 테이블, 컬럼 이름으로 바꿀 때 이름을 그대로 유지하도록 하는 설정이다.

Hibernate

자바 객체와 DB 테이블 사이에서 SQL 생성과 조회를 실제로 처리해 주는 ORM 도구,
JPA → 자바 ORM 표준 규칙
Spring Data JPA → JPA를 편하게 쓰게 해줌
Hibernate → JPA 규칙을 실제로 구현
MySQL → 실제 데이터를 저장

3. 회원가입 로직 구현

1) DTO

JoinDTO

@Getter
@Setter
public class JoinDTO {
    
    private String username;
    private String password;
    
}

사용자로부터 username 값과 password 값을 받아 옮긴다.

2) Controller

JoinController

@Controller
@ResponseBody
public class JoinController {

    private final JoinService joinService;

    public JoinController(JoinService joinService) {
        this.joinService = joinService;
    }

    @PostMapping("/join")
    public String joinProcess(JoinDTO joinDTO) {
        joinService.joinProcess(joinDTO);

        return "ok";
    }
}

3) Service

UserRepository 코드 추가

Boolean existsByUsername(String username);

username이 존재하는지 o/x로 알려주는 기능을 사용한다.

JoinService

@Service
public class JoinService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public JoinService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    public void joinProcess(JoinDTO joinDTO) {

        String username = joinDTO.getUsername();
        String password = joinDTO.getPassword();

        Boolean isExist = userRepository.existsByUsername(username);

        if (isExist) {

            return;
        }

        UserEntity data = new UserEntity();
        data.setUsername(username);
        data.setPassword(bCryptPasswordEncoder.encode(password));
        data.setRole("ROLE_ADMIN");
    }
}

4. 로그인 로직 구현

1) 로그인 동작 원리


이번에 구현해야 할 중요한 것은 UsernamePassword AuthenticationFilter와 Authentification Manager이다.

저번 일반 Spring Security form Login 로직을 구현할 때는 Spring Sercurity가 이 부분을 만들어주었기 때문에 구현하지 않았지만 이번 JWT 로그인에서는 form Login 부분을 disable로 설정해두었기 때문에 커스텀을 통해 새로 구현해야 한다.

간단한 작동 순서로는 우선 사용자가 username과 password를 제시하면 UsernamePassword AuthenticationFilter가 username과 password를 추출하여 Authentication Manager가 DB와 매치할 수 있도록 한다.
값이 일치한다면 successfulAuth를 반환하여 JWT 토큰을 발급한다.
만약 값이 일치하지 않는 401 응답코드를 반환한다.

로그인 로직 구현 목표

  • 아이디, 비밀번호 검증을 위한 커스텀 필터 작성
  • DB에 저장되어 있는 회원 정보를 기반으로 검증할 로직 작성
  • 로그인 성공시 JWT를 반환할 success 핸들러 생성
  • 커스텀 필터 SecurityConfig에 등록

2) AuthenticationFilter 커스텀 구현

SecurityConfig에 코드추가

//AuthenticationManager Bean 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

        return configuration.getAuthenticationManager();
    }

Spring Security가 만들어 둔 AuthenticationManager를 꺼내서 Bean으로 등록

AuthenticationConfiguration 안에는 authenticationManager, authenticationProvider 등 여러 authentication 관련 객체가 있고 그 설정들을 바탕으로 authenticationManager를 꺼낸 것이다.

//필터 추가 LoginFilter()는 인자를 받음 (AuthenticationManager() 메소드에 authenticationConfiguration 객체를 넣어야 함) 따라서 등록 필요
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

내 필터(authenticationConfiguration)를 기준필터 자리인(UsernamePasswordAuthenticationFilter)에 넣겠다.

LoginFilter(jwt)

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    public LoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

	// 로그인 요청이 들어왔을 때 실행되는 인증 시도 메서드
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws AuthenticationException {

        String username = obtainUsername(request);
        String password = obtainPassword(request);

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

        return getAuthenticationManager().authenticate(authToken);
    }

    // 로그인 성공 시 JWT 발급
    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authentication
    ) {
    }

    // 로그인 실패 처리
    @Override
    protected void unsuccessfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException failed
    ) {
    }
}

LoginFilter가 Spring Security의 기본 로그인 필터인 UsernamePasswordAuthenticationFilter를 상속받고

LoginFilter가 인증을 수행하려면 AuthenticationManager가 필요하므로
super(authenticationManager)가 받은 AuthenticationManager를 부모 클래스인 UsernamePasswordAuthenticationFilter에게 전달한다.

5. DB 기반 로그인 검증로직

세션 로그인과 비슷한 방식으로 진행

CustomUserDetailsSevice

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //DB에서 조회
        UserEntity userData = userRepository.findByUsername(username);

        if (userData != null) {

            //UserDetails에 담아서 return하면 AutneticationManager가 검증 함
            return new CustomUserDetails(userData);
        }

        return null;
    }
}

CustomUserDetails(DTO)

public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {

        this.userEntity = userEntity;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {

            @Override
            public String getAuthority() {

                return userEntity.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {

        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {

        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {

        return true;
    }

    @Override
    public boolean isAccountNonLocked() {

        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {

        return true;
    }

    @Override
    public boolean isEnabled() {

        return true;
    }
}

로그인 요청이 들어오면 LoginFilter가 요청에서 username과 password를 추출하여 AuthenticationManager에 전달하고 이후 Spring Security 내부에서 UserDetailsService를 통해 데이터베이스의 사용자 정보를 조회한다.

CustomUserDetailsService는 Spring Security의 UserDetailsService 인터페이스를 구현한 클래스.

loadUserByUsername() 메서드는 로그인 요청으로 전달된 username을 이용해 UserRepository에서 사용자 정보를 조회한다. 조회된 사용자가 존재하면 UserEntity를 그대로 반환하는 것이 아니라

Spring Security가 이해할 수 있는 UserDetails 형태로 변환하여 반환해야 한다. 이를 위해 조회한 UserEntity를 CustomUserDetails 객체에 담아 반환한다.

반환된 CustomUserDetails는 저번 세션 로그인 구현 때와 동일하게 AuthenticationProvider에서 DB 비밀번호와 사용자가 로그인 요청에서 입력한 비밀번호를 비교하는데 사용된다.

getUsername()은 사용자의 아이디, getPassword()는 암호화 비밀번호, getAuthorities()는 권한 정보를 반환한다.

isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled()는 각각 계정 만료 여부, 계정 잠금 여부, 비밀번호 만료 여부, 계정 활성화 여부를 나타낸다.

현재 코드에서는 모두 true를 반환하기 때문에 모든 계정이 만료X / 비밀번호 유효 / 활성화된 상태

전체 흐름

  • 로그인 요청에서 전달된 username/password가 LoginFilter를 통해 AuthenticationManager로 전달
  • AuthenticationManager는 AuthenticationProvider에 인증 처리를 부탁
  • AuthenticationProvider는 CustomUserDetailsService의 loadUserByUsername()을 호출하여 DB에서 사용지를 조회
  • 조회 결과를 CustomUserDetails 형태로 받음
  • PasswordEncoder를 이용해 입력 비밀번호와 DB의 암호화 비밀번호를 비교하며 검증에 성공하면 인증된 Authentication 객체를 생성

6. JWT 생성과 검증 클래스

로그인시 → 성공 → JWT 생성
접근시 → JWT 검증

JWT에 대해 생성과 검증을 담당할 클래스가 필요하다. 따라서 JWTUtil이라는 클래스를 생성하여 JWT 발급, 검증 메소드를 작성해야 한다.

1) JWT 구조와 기능

JWT는 Header.Payload.Signature 구조로 이루어져 있으며
각 요소는 다음 기능을 수행한다.

  • 토큰의 타입이 JWT임을 명시
  • 서명에 사용된 알고리즘 정보

Payload

  • 사용자 정보나 권한 등의 데이터
  • 비밀번호와 같은 민감한 정보는 넣으면 안됌

Signature(서명)

  • 이 토큰이 서버가 만든 게 맞는지
  • 중간에 내용이 바뀌지 않았는지
  • 서명알고리즘((BASE64(Header))+(BASE64(Payload)) + 비밀키)

JWT는 내부 정보를 단순 BASE64 방식으로 인코딩하기 때문에 외부에서 쉽게 디코딩 할 수 있다.
-> 외부에서 열람해도 되는 정보를 담아야하며 토큰 자체의 발급처를 확인하기 위해서 사용한다.

(지폐와 같이 외부에서 그 금액을 확인하고 금방 외형을 따라서 만들 수 있지만 발급처에 대한 보장 및 검증은 확실하게 해야하는 경우에 사용한다. 따라서 토큰 내부에 비밀번호와 같은 값 입력 금지)

BASE64

누구나 쉽게 디코딩할 수 있으면 왜 인코딩하는걸까??
Base64로 인코딩한다 -> 데이터를 영문자·숫자·기호로 이루어진 문자열 형태로 바꾼다
Base64를 사용하는 이유는 이미지나 파일처럼 그대로 전달하기 어려운 데이터를 문자열만 전달할 수 있는 환경에서도 안전하게 보내기 위해서

인코딩과 암호화는 같은 뜻이 아님
인코딩: 데이터의 형태를 바꾸겠다
암호화: 외부에서 값을 알아볼 수 없도록 바꾸겠다.

2) JWT 서명 알고리즘(Signature)

비밀키 - 서버만 알고 있는 비밀번호 같은 문자열

서명

  • 대칭키 기반 서명
    • HS256
    • 하나의 비밀키로 서명 생성 및 검증
      .
  • 비대칭키 기반 서명
    • RS256
    • 개인키로 서명 생성, 공개키로 검증

-> 서명을 만들 때 쓰는 키와 검증할 때 쓰는 키가 같냐, 다르냐

JWT 기반 로그인의 중요한 점은 데이터를 암호화하는 것이 아닌 위조되지 않았음을 확인시키는 것

Payload에 있는 값 + 비밀키 -> signature 생성
만약 Payload에 있는 값이 달라지면 signature이 달라지기 때문에 토큰을 믿을 수 없다고 판단한다.

3) 암호화 키 저장

application.properties

코드 추가

spring.jwt.secret=vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb

여기서는 그냥 적었지만 암호화 키는 구현 내부에 탑재하는 것(하드코딩)을 지양하기 때문에 환경변수 이름만 적어두고 .gitignore 파일에 저장해야 한다.

4) JWTUtil

-> JWT 생성과 검증 클래스

토큰 Payload에 저장될 정보

  • username
  • role
  • 생성일
  • 만료일

JWTUtil 구현 메소드

  • JWTUtil 생성자(JWT 서명에 사용할 비밀키를 준비하는 것)
  • username 확인 메소드
  • role 확인 메소드
  • 만료일 확인 메소드
@Component
public class JWTUtil {

	// 비밀키 객체 생성
    private SecretKey secretKey;

	// 생성자 실행
    public JWTUtil(@Value("${spring.jwt.secret}")String secret) {

        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

	// JWT 정보조회(username 조회)
    public String getUsername(String token) {

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

	// JWT 정보조회(role 조회)
    public String getRole(String token) {

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

	// JWT 정보조회(만료여부 조회)
    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

	// 모든 검사가 통과되면 builder에게 반환하여 JWT 발급
    public String createJwt(String username, String role, Long expiredMs) {

        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();
    }
}
// 생성자 실행
    public JWTUtil(@Value("${spring.jwt.secret}")String secret) {

        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

생성자가 실행되면 application.properties에서 Spring.jwt.secret 값을 가져온다. JWT 객체를 만든다.

이 값을 사용해서 비밀키를 만든다.
앞서 받은 String secret 변수를 바이트로 나눠준다.
HS256 알고리즘을 가져와서 secretKey를 생성한다.

// JWT 정보조회(username 조회)
    public String getUsername(String token) {

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

paser()는 Jwt를 받아서 해석하는 도구를 만들겠다는 뜻
.parseSignedClaims(token) 해석을 수행

  • JWT 형식이 올바른지
  • 서명이 올바른지
  • 토큰 구조에 문제가 없는지

5) 로그인 성공과 실패

// JWTUtill 주입
    private final JWTUtil jwtUtil;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) {
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtUtil = jwtUtil;
    }
//AuthenticationManager()와 JWTUtil 인수 전달
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

        return http.build();

직접 만든 LoginFilter를 기존의 UsernamePasswordAuthenticationFilter 위치에 등록하고 LoginFilter에 authenticationManager(username/password 검증)와 JWTuUtil(JWT 토큰 발급)을 받아온다.

(어려워)

7. JWT 검증 필터

스프링 시큐리티 filterChain에 요청에 담긴 JWT를 검증하기 위한 커스텀 필터를 등록해야 한다.

해당 필터를 통해 요청 헤더 Authorization 키에 JWT가 존재하는 경우 JWT를 검증하고 SecurityContextHolder에 세션을 생성한다. (이 세션은 STATLESS 상태로 관리되기 때문에 해당 요청이 끝나면 소멸된다.)

JWTFilter

public class JWTFilter extends OncePerRequestFilter {
    
    private final JWTUtil jwtUtil;

    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }


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

        //request에서 Authorization 헤더를 찾음
        String authorization= request.getHeader("Authorization");

        //Authorization 헤더 검증
        if (authorization == null || !authorization.startsWith("Bearer ")) {

            System.out.println("token null");
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        System.out.println("authorization now");
        //Bearer 부분 제거 후 순수 토큰만 획득
        String token = authorization.split(" ")[1];

        //토큰 소멸 시간 검증
        if (jwtUtil.isExpired(token)) {

            System.out.println("token expired");
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        //userEntity를 생성하여 값 set
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername(username);
        userEntity.setPassword("temppassword");
        userEntity.setRole(role);

        //UserDetails에 회원 정보 객체 담기
        CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

        //스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
        
        // 현재 요청의 SecurityContext에 인증 정보 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}

요청에서 JWT 찾기 → JWT 검증 → 사용자 정보 꺼내기 → Authentication 생성 → SecurityContext에 저장 → 다음 필터로 넘기기

public class JWTFilter extends OncePerRequestFilter {

모든 요청마다 한번씩 실행되는 필터 상속받기
사용자가 요청을 보내면 JWTFilter가 중간에 JWT 토큰을 검사한다.

private final JWTUtil jwtUtil;

public JWTFilter(JWTUtil jwtUtil) {
    this.jwtUtil = jwtUtil;
}

JWTUtil을 사용하여 토큰 해석, 서명 검증, 만료 확인 등 JWT를 검사한다.

그 후 @Override를 사용하여 사용자가 보낸 요청(Request)과 보낼 응답(Response), 검증(filterChain)을 수행하는 메서드를 만든다.

Request에서 header에 Bearer 형식의 토큰이 있는지 검사하고 Bearer 부분 제거 후 순수 토큰만을 얻어 JWTUtil에서 검증한다.

검증이 완료되면 username과 role을 얻어 userEntity를 생성하여 정보를 담는다. new UserEntity이므로 DB에 저장되는 정보는 아니고 임시적으로 로그인 되었음을 알리기 위한 Entity이다.

SecurityConfig

코드추가

//JWTFilter 등록
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

jWTFilter가 LonginFilter보다 먼저 실행될 수 있도록 SecurityConfig에 설정을 추가해준다.

8. 세션 정보

Main Controller

@Controller
@ResponseBody
public class MainController {

    @GetMapping("/")
    public String mainP() {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        String name = SecurityContextHolder.getContext().getAuthentication().getName();

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

        return "username : " + name + ", role : " + role;
    }
}

앞에서 JWTFilter가 정상적인 JWT를 확인하면 그러면 Spring Security의 SecurityContext 안에 현재 요청 사용자의 인증 정보가 저장된다. Authentication 안에 CustomUserDetails가 들어 있으므로 getName()은 CustomUserDetails.getUsername() 값을 반환한다.

9. CORS 설정

CORS란?

Cross-origin resource sharing = 교차 출처 리소스 공유
브라우저가 다른 출처의 서버로 요청을 보낼 때 적용하는 보안 규칙
허용하는 서버의 요청을 지정해주어야 한다.
ex) 프론트엔드와 백엔드의 실행 서버가 다르기 때문에 다른 출처라고 본다.

SpringConfig

코드추가

http
                .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {

                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

                        CorsConfiguration configuration = new CorsConfiguration();

                        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
                        configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowCredentials(true);
                        configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setMaxAge(3600L);

                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));

                        return configuration;
                    }
                })));

CorsMvcconfig

< Config

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {

        corsRegistry.addMapping("/**")
                .allowedOrigins("http://localhost:3000");
    }
}

요청을 허용할 프론트의 경로 입력

0개의 댓글