본문 바로가기
개발

스프링 시큐리티(Spring Security) + JWT

by hwan-da 2024. 7. 21.

저번주에 이어 SpringSecurity와 JWT를 사용하여 간단한 회원가입과 로그인, JWT 토큰 검증을 공부했다.

신기한 건 저번주까지만 해도 ai 학습이 2023년 9월까지여서 현재 SpringSecurity 버전에 대한 코드를 주지 못했다고 했는데,

2024년 3월까지로 학습이 변경되어 이제 최신 버전의 SpringSecuirty 코드를 얻을 수 있게 되었다(!)


다음주는 SpringSecurity, JWT에 소셜 로그인까지 더하여 공부할 계획이다.

실습 목표 및 간단한 동작 원리

  • 실습 목표
    • 스프링 시큐리티 6 프레임워크를 활용하여 JWT 기반의 인증/인가를 구현하고 회원 정보 저장(영속성) MySQL 데이터베이스를 활용
    • 서버는 API 형태로 구축(웹 페이지를 응답하는 것이 아닌 API 클라이언트 요청을 통해 데이터 응답만 확인
  • 구현
    • 인증 : 로그인
    • 인가 : JWT를 통한 경로별 접근 권한
    • 회원가입
  • JWT 인증 방식 시큐리티 동작 원리
    • 회원가입 : 내부 회원 가입 로직은 세션 방식과 JWT 방식의 차이가 없음
    • 로그인 (인증) : 로그인 요청을 받은 후 세션 방식은 서버 세션이 유저 정보를 저장하지만 jwt 방식은 토큰을 생성하여 응답
      • AuthenticationFilter를 통해 특정한 회원 검증 로직 작성
      • AuthentifiactionManager를 통해 내부적으로 로그인 검증을 함
        • DB에서 UserDetailService가 userDetail에 담아서 진행 → 세션 방식과 동일함
      • 로그인 성공 시, 서버 세션이 아닌 SuccessfulAuthentification을 통해 JwtUtil에서 토큰을 만들어 응답해줌
    • 경로 접근 (인가)
      • JWT Filter를 통해 요청의 헤더에서 JWT를 찾아 검증하고 일시적 요청에 대한 Session 생성
        • JWT Token을 만들어 Filter에서 검증
        • 토큰 내부 정보가 일치하면 JWT Filter에서 강제로 SecurityContextHolder에 일시적인 세션을 만들어 특정한 경로로 요청이 들어가면 그 세션이 존재하기 때문에 접근이 가능해짐
        • 이 방식은 세션을 stateless 방식으로 관리하기 때문에, 하나의 요청에 대한 세션만 만들고 끝나면 사라짐
        • 동일한 아이디가 들어와도 계속해서 새로 만들고 끝나면 사라짐

프로젝트 생성 및 의존성 추가

  • JWT 토큰을 생성하고 관리하기 위해 JWT 의존성을 필수적으로 설정해야 함

  • 0.12.3을 기반으로 구현 → 0.11.5가 많지만 구현하는 메서드가 버전마다 상이

  • 의존성 추가

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

SecurityConfig 클래스

package com.example.springjwt.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

        // csrf disable
        http    
                // JWT는 statless 상태로 관리하기 때문에 csrf 공격을 고려하지 않아도 됨
                .csrf((auth) -> auth.disable());

        http
                .formLogin((auth) -> auth.disable());

        http
                .httpBasic((auth) -> auth.disable());

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

        http
                // JWT 방식에서는 Session을 statless 상태로 관리
                // Session을 statless 상태로 만들어줘야 함
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); 

        return http.build();
    }
}
  • Session을 statless 상태로 관리하기 위한 설정을 한 것이 가장 중요

POSTMAN

API 서버는 웹서버와 달리 서버 측으로 요청을 보낼 수 있는 페이지가 존재하지 않음


엔드포인트만 존재하기 때문에 요청을 보낼 API가 필요함

DB 연결 및 Entity 작성

  • 데이터베이스 종류와 ORM

    • 회원 정보를 저장하기 위한 데이터베이스는 MySQL 데이터베이스 사용
    • 연결은 JPA를 통해 진행
  • 변수 설정

    • Hibernate ddl 설정

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

  • UserRepository 작성

  • ddl-auto=create 설정 후 실행

    • 데이터베이스에서 회원 정보를 저장할 테이블을 생성해야 하지만 ddl-auto 설정을 통해 스프링 부트 Entity 클래스 기반으로 테이블 생성 가능

회원가입 로직 구현

  • dto → JoinController → JoinService → UserEntity → UserRepository → DB

  • DTO, Controller, Service 작성

    • DTO

        package com.example.springjwt.dto;
      
        import lombok.Getter;
        import lombok.Setter;
      
        @Getter
        @Setter
        public class JoinDto {
      
            private String username;
            private String password;
      
        }
      
    • JoinController

        package com.example.springjwt.controller;
      
        import com.example.springjwt.dto.JoinDto;
        import com.example.springjwt.service.JoinService;
        import lombok.RequiredArgsConstructor;
        import org.springframework.stereotype.Controller;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PostMapping;
        import org.springframework.web.bind.annotation.ResponseBody;
      
        @Controller
        @ResponseBody
        @RequiredArgsConstructor
        public class JoinController {
      
            private final JoinService joinService;
      
            @PostMapping("/join")
            public String joinProcess(JoinDto joinDto) {
                joinService.joinProcess(joinDto);
                return "ok";
            }
      
        }
      
    • JoinService

        package com.example.springjwt.service;
      
        import com.example.springjwt.dto.JoinDto;
        import com.example.springjwt.entity.UserEntity;
        import com.example.springjwt.repository.UserRepository;
        import lombok.RequiredArgsConstructor;
        import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
        import org.springframework.stereotype.Service;
      
        @Service
        @RequiredArgsConstructor
        public class JoinService {
      
            private final UserRepository userRepository;
            private final BCryptPasswordEncoder bCryptPasswordEncoder;
      
            public void joinProcess(JoinDto joinDto) {
                String username = joinDto.getUsername();
                String password = joinDto.getPassword();
      
                Boolean isExists = userRepository.existsByUsername(username);
      
                if(isExists) {
                    return;
                }
      
                UserEntity data = new UserEntity();
      
                data.setUsername(username);
                data.setPassword(bCryptPasswordEncoder.encode(password));
                data.setRole("ROLE_ADMIN");
      
                userRepository.save(data);
            }
      
        }
      

로그인 로직 구현

UsernamePasswordAuthenticationFilter, AuthenticationManager를 커스텀하여 로그인 구현

  • SpringSecurity Filter 동작 원리
    • SpringSecurity는 클라이언트의 요청이 여러 개의 필터를 거치게 됨
    • DispatcherSevlet으로 향하는 중간 필터에서 요청을 가로챈 후 검증(인증/인가)를 진행
  • 클라이언트 요청 → 서블릿 필터 → 서블릿(컨트롤러)
  • Delegating Filter Porxy
    • 서블릿 컨테이너(톰캣)에 존재하는 필터 체인에 DelegatingFilter를 하나 등록한 뒤 모든 요청을 가로챔
    • SpringSecurity 의존성 추가 시 이 DelegatingFilter를 하나 등록
  • 서블릿 필터 체인의 DelegatingFilter → SecurityFilterchain(내부 처리 후 ) → 서블릿 필터 체인의 DelegatingFilter
    • 가로챈 요청은 SecurityFilterChain에서 처리 후 상황에 따른 거부, 리디렉션, 서블릿으로 요청 진행
    • DelegaingFilter는 서블릿, SecurityFilter는 SpringSecurity로 다른 것임

Form 로그인 방식에서 UsernamePasswordAuthenticationFilter

  • Form 로그인 방식에서는 클라이언트 단이 username과 password를 전송한 뒤 Secuirty 필터를 통과
  • 이때, UsernamePasswordAuthenticationFilter에서 회원 검증을 진행
  • formLogin 방식을 config 설정에서 disable 했다면, 이 필터는 동작하지 않음
    • 즉, 로그인을 진행하기 위해서 필터를 커스텀 해서 등록해야 함

로그인 로직 구현 목표

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

로그인 요청 받기 : 커스텀 UsernamePasswordAuthenticationFilter

  • LoginFilter

      package com.example.springjwt.jwt;
    
      import jakarta.servlet.FilterChain;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      import lombok.RequiredArgsConstructor;
      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;
    
      @RequiredArgsConstructor
      public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
          // 인증을 위한 AuthentiacationManager 선언
          private final AuthenticationManager authenticationManager;
    
          // 필수 구현
          @Override
          public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    
              // 클라이언트 요청에서 username과 password 추출
              String username = request.getParameter("username");
              String password = request.getParameter("password");
    
              // SpringSecurity에서 username과 password를 검증하기 위해서는 token(DTO)에 담아야 함
              // UsernamePassswordAuthenticationToken이 이를 담당
              UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
    
              System.out.println(username);
    
              // token에 담은 정보를 검증하기 위해 AuthenticationManager에게 전달
              // 검증 방법은 DB에서 user 정보를 확인함
              return authenticationManager.authenticate(authToken);
          }
    
          // 로그인 성공 시 진행하는 메서드(여기서 JWT 토큰 발행)
          @Override
          protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
          }
    
          // 로그인 실패 시 진행하는 메서드
          @Override
          protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
          }
      }
    
  • 작성한 필터를 등록해주어야 함

    • SecurityConfig에서 등록

      • SecurityFilterChain을 return하는 메서드에 등록

        private final AuthenticationConfiguration authenticationConfiguration;
        
        @Bean
        public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
         return configuration.getAuthenticationManager();
        }
        
        // 기존 SecurityConfig에 아래 내용추가
        http
                      // addFilter
                      // addFilterAt : 원하는 자리에 filter 등록
                      // addFilterBefore : 해당 filter 전에 등록
                      // addFilterAfter : 해당 filter 후에 등록
                      // 여기서는 usernamePasswordAuthticationFilter 자리에 대체하는 filter를 넣을 것이기 때문에 addFilterAt을 사용
                      // 첫 번째 인자는 filter, 두 번째 인자는 어디에 넣을 것인가
                      // LoingFilter를 만들 때 AuthenticationManager를 인자로 받았기 떄문에 이를 등록해서 넣어주어야 함
                      // authenticationManager도 인자가 있기 때문에 AuthenticationConfiguration 생성자 주입
                      .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);
  • 로그인 성공 시 JWT 반환

DB기반 로그인 검증 로직

DB에서 특정한 user를 조회하는 user 조회 ORM 작성

  • userRepository에 user를 조회하는 로직 추가

      UserEntity findByUsername(String username);
  • UserDetailsService 커스텀 구현

      package com.example.springjwt.service;
    
      import com.example.springjwt.dto.CustomUserDetails;
      import com.example.springjwt.entity.UserEntity;
      import com.example.springjwt.repository.UserRepository;
      import lombok.RequiredArgsConstructor;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Service;
    
      @Service
      @RequiredArgsConstructor
      public class CustomUserDetailsService implements UserDetailsService {
    
          private final UserRepository userRepository;
    
          // 필수 구현 메서드
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              // DB에서 특정 user를 조회하여 반환
    
              UserEntity userData = userRepository.findByUsername(username);
    
              if(userData != null) {
                  // userDetails라는 걸 만들어서 userDetailsService에서 최종적으로 Authentication으로 넘겨주는데 이것도 커스텀해서 구현해야 함
                  // 데이터를 넘겨주는 DTO에 해당함
                  return new CustomUserDetails(userData);
              }
    
              return null;
          }
      }
  • CustomUserDetails 구현

      package com.example.springjwt.dto;
    
      import com.example.springjwt.entity.UserEntity;
      import lombok.RequiredArgsConstructor;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.userdetails.UserDetails;
    
      import java.util.ArrayList;
      import java.util.Collection;
      import java.util.List;
    
      @RequiredArgsConstructor
      public class CustomUserDetails implements UserDetails {
    
          private final 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;
          }
      }

JWT 발급 및 검증 클래스

강의 내 버전이 0.12.3 버전이므로 최신 버전인 0.12.6 버전과는 메서드가 다를 수 있음


로그인 시 → 성공 → JWT 발급, 접근 시 → JWT 검증

  • JwtUtil이라는 클래스를 생성하여 JWT를 발급, 검증

JWT 생성 원리

  • JSON 타입의 web Token
  • 문자열 형태를 띔
  • 세 가지 부분으로 분리
    • Header
      • JWT임을 명시
      • 사용된 암호화 알고리즘
    • Payload
      • 정보
      • 클레임(Claim)
        • 토큰에서 사용할 정보의 조각들
        • Key / Value 형태
        • 저장되는 정보에 따른 분류
          • 등록된 클레임(Registered Claims)
          • 공개 클레임(Public Claims)
          • 비공개 클레임(Private Cliams)
    • Signature
      • 암호화 알고리즘((BASE64(Header)) + (BASE64(Payload)) + 암호화키)
  • Header와 Payload는 외부에서도 열람 가능
    • 즉, 외부에 노출되어도 괜찮은 정보만을 담아야 함
    • 토큰 자체의 발급처를 확인하기 위해서 사용

JWT 암호화 방식

  • 암호화 종류
    • 양방향
      • 대칭키 → 암호화, 복호화 같은 키 사용
        • 이 프로젝트에서는 양방향 대칭키 방식 사용 : HS256
      • 비대칭키 → 암호화, 복호화 다른 키 사용
    • 단방향

암호화 키 저장

  • 암호화 키는 하드코딩 방식으로 구현 내부에 탑재하는 것을 지양하기 때문에 변수 설정 파일에 저장

    • 외부 유출 가능성

    • MSA 방식 사용하여 여러 서버를 띄울 때, 암호 변경 시 수정이 어려움

    • application.properties

        spring.jwt.secret

JWTUtil

  • 토큰 Payload에 저장할 정보

    • username
    • role
    • 생성일
    • 만료일
  • JWTUtil 구현 메소드

    • JWTUtil 생성자
    • username 확인 메소드
    • role 확인 메소드
    • 만료일 확인 메소드
    • jwt 구현 메소드
  • JwtUtil

      package com.example.springjwt.jwt;
    
      import io.jsonwebtoken.Jwts;
      import lombok.RequiredArgsConstructor;
    
      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 JwtUtil {
    
          private SecretKey secretKey;
    
          // anotation 주의, lombok Value 아님
          // SecretKey는 String안 쓰고 SecretKey라는 클래스를 쓴다
          public JwtUtil(@Value("${spring.jwt.secret}") String secret) {
              secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
          }
    
          public String getUsername(String token) {
              // verifyWith : 생성한 곳이 우리가 맞는지 확인
              // parseSignedClaims : 클레임을 해석, 검증
              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 isTokenExpired(String token) {
              // before : 다른 Date 객체보다 이전인지 여부 확인
              return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
          }
    
          // jwt 토큰 생성 메서드
          public String createToken(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)
                      // JWT를 빌드하여 최종적으로 문자열 형태로 압축
                      .compact();
          }
    
      }
    

로그인 성공 시 JWT 발급

JwtUtil 주입

  • LoginFilter
    • LoginFilter에 주입 시 SecurityConfig에도 주입해주어야 함

LoginFilter 로그인 성공 successfulAuthentication 메소드 구현

  • LoginFilter

      // 로그인 성공 시 진행하는 메서드(여기서 JWT 토큰 발행)
          @Override
          protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
    
              // user 객체를 알아내기 위함
              // getPrinciapl() : 현재 인증된 사용자 또는 주체(principal)에 대한 정보 반환
              CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
    
              // 유저 이름
              String username = customUserDetails.getUsername();
    
              // collection에서 authority 객체를 뽑아낸 후 iterator를 사용하여 내부 객체를 뽑아내면 됨
              Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
              Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
              GrantedAuthority auth = iterator.next();
              String role = auth.getAuthority();
    
              String token = jwtUtil.createToken(username, role, 60 * 60 * 10L);
    
              // 응답의 헤더에 담아 token 반환, 뒤의 양식은 꼭 지키기
              // HTTP 인증 방식은 RFC 7235 정의에 따라 양식이 정해진 것
              response.addHeader("Authorization", "Bearer " + token);
          }
    
          // 로그인 실패 시 진행하는 메서드
          @Override
          protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
              response.setStatus(401);
          }

JWT 검증 필터

SpringSecurityFilterChain에 요청에 담긴 JWT 검증을 위한 커스텀 필터 등록 필요


요청 헤더 Authrization 키에 JWT 가 존재하는 경우 JWT를 검증


강제로 SecurityContextHolder에 세션을 생성


→ 이 세션은 STATLESS 상태로 관리되어 해당 요청이 끝나면 소멸


JWT 필터 구현

  • JwtFilter

      package com.example.springjwt.jwt;
    
      import com.example.springjwt.dto.CustomUserDetails;
      import com.example.springjwt.entity.UserEntity;
      import jakarta.servlet.FilterChain;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      import lombok.RequiredArgsConstructor;
      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;
    
      // 요청에 대해 한 번만 동작하는 OncePerRequestFilter 상속
      @RequiredArgsConstructor
      public class JwtFilter extends OncePerRequestFilter {
    
          private final 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");
                  // request와 response를 다음 filter로 넘김
                  filterChain.doFilter(request, response);
    
                  // 조건이 해당되면 메서드 종료(필수)
                  return;
              }
    
              System.out.println("authorization start");
    
              String token = authorization.split(" ")[1];
    
              // JWT 토큰 소멸 시간 검증
              if(jwtUtil.isTokenExpired(token)) {
                  System.out.println("token expired");
                  filterChain.doFilter(request, response);
    
                  // 조건이 해당되면 메서드 종료(필수)
                  return;
              }
    
              // 두 개의 if 문을 거치면 토큰 확인 완료
              // 이 토큰을 기반으로 일시적인 세션을 만들어 SecurityContextHolder라는 Security 세션에 User를 일시적으로 저장
              // 유저 정보를 요구하는 경로의 요청을 진행 가능
    
              // 토큰에서 username과 role 획득
              String username = jwtUtil.getUsername(token);
              String role = jwtUtil.getRole(token);
    
              // userEntity를 생성하여 값 set
              UserEntity userEntity = new UserEntity();
              userEntity.setUsername(username);
              // 임시로 password를 강제로 넣어주어 사용 / DB를 다녀오는 건 비효율적
              userEntity.setPassword("temppassword");
              userEntity.setRole(role);
    
              CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
    
              // 스프링 시큐리티 인증 토큰 생성
              Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
    
              // 세션에 사용자 등록
              SecurityContextHolder.getContext().setAuthentication(authToken);
    
              // 다음 필터 실행
              filterChain.doFilter(request, response);
          }
    
      }
    
  • SecurityConfig

      http
              // jwt 검증 로직 LoginFilter 앞에 추가
              .addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class);

세션 정보

JwtFilter를 통과한 뒤 세션 확인


컨트롤러에서 특정한 id나 role값을 확인


JwtFilter를 통과한 순간 일시적으로 세션을 만들기 때문에, SecurityContextHolder의 세션을 통해 유저의 id나 role을 확인할 수 있음

@GetMapping("/")
    public String mainP() {
    String username = SecurityContextHolder.getContext().getAuthentication().getName();

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

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

    String role = auth.getAuthority();

    return "Main Controller" + " " + username + " " + role;
}

CORS 설정

CORS란?

  • 프론트서버(3000번대) → 웹브라우저 → 백서버(8000번대)
    • 두 서버 포트 번호가 다름 → 웹 브라우저에서 교차 출처 리소스 공유 금지 시킴
    • 백엔드에서 CORS 처리를 시켜주어야 데이터를 보여줄 수 있음

설정 방법(두 가지 모두 해야 함)

  • 이유

    • 기본적으로 MvcConfigure에서 처리
    • 로그인 같은 Security를 거치는 것은 Security에서 설정하지 않으면 토큰을 리턴하지 않을 수 있음
  • Security에서 설정

    • filterChain 메서드에 CORS 설정 추가

        http
                        .cors((cors) -> cors
                                .configurationSource(new CorsConfigurationSource(){
      
                                    @Override
                                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                                        CorsConfiguration configuration = new CorsConfiguration();
      
                                        // 프론트 서버 쪽 포트 허용
                                        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
                                        // 허용할 메소드 설정
                                        configuration.setAllowedMethods(Collections.singletonList("*"));
                                        // credencial 설정
                                        configuration.setAllowCredentials(true);
                                        // 헤더 허용
                                        configuration.setAllowedHeaders(Collections.singletonList("*");
                                        // 헤더를 갖고 있을 시간
                                        configuration.setMaxAge(3600L);
      
                                        // 클라이언트로 header를 보낼 때 Jwt토큰을 담아서 보내므로, Authorization header를 허용
                                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));
      
                                        return null;
                                    }
                                }));
  • CorsConfig 생성 및 설정

      package com.example.springjwt.Config;
    
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.CorsRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
      @Configuration
      public class CorsConfig implements WebMvcConfigurer {
    
          @Override
          public void addCorsMappings(CorsRegistry corsRegistry) {
    
              corsRegistry.addMapping("/**")
                      // 프론트 포트번호 허용해주면 됨
                      .allowedOrigins("http://localhost:3000");
    
          }
    
      }

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

[OAuth2] X(구 트위터) 로그인 API 사용기  (1) 2024.11.17
Docker 공부하기 (기초)  (1) 2024.09.01
JWT 심화  (1) 2024.08.11
OAuth2.0 + SpringSecurity  (0) 2024.08.04
스프링 시큐리티(Spring Security) 기본  (5) 2024.07.14