JWT 로그인 하는 방법1

Park sang woo·2024년 8월 16일
0

CS스터디

목록 보기
14/25

⭕ JWT 로그인 방법

⚒️ JWT 인증 방식 시큐리티 동작 원리

회원가입
세션 방식과 큰 차이 없음.


로그인
세션 사용 시 UsernamePasswordAuthenticationFilter를 다 구현하지 않고도 SpringSecurity가 알아서 처리를 해줬습니다.

JWT 방식은 로그인 경로로 요청이 오면 UsernamePasswordAuthenticationFilter를 통해서 특정한 회원 검증을 하는 로직을 작성해야 합니다.

AuthenticationManager를 통해서 내부에 ID와 Password를 던져서 로그인 검증을 해야 합니다.
검증을 하는 방법은 DB에 저장되어 있는 User 정보를 꺼내와서 UserDetailsServiceUserDetails에 담아서 최종적으로 AuthenticationManager에서 검증을 합니다.

로그인이 성공하면 기존 세션 방식은 서버 세션에다가 회원 정보를 저장하지만 JWT 방식은 회원 정보를 남기지 않고 SuccessfulAuthentication 이라는 메서드를 통해서 JWTUtils에서 토큰을 만들어 응답을 합니다.

UsernamePasswordAuthenticationFilterAuthenticationManager와 회원 검증하는 부분인 UserDetailsService까지는 세션과 동일합니다.



토큰을 가지고 특정한 다른 어드민 경로, 게시판 경로에 접근할 때 토큰을 헤더에 넣어서 요청을 진행합니다.
특정한 경로로 요청이 오면 가장 첫 번째로 SecurityAuthenticationFilter가 검증을 한 번 진행하고 JWTFilter를 커스텀해서 필터 검증을 진행합니다.
토큰이 알맞게 존재하고 토큰 정보가 정확히 일치하면 JWTFilter에서 강제로 일시적인 요청에 대한 Session을 SecurityContextHolder에 생성합니다.
이 방식은 Session을 STATELESS 상태로 관리하기 때문에 단 하나의 요청에 대해서 일시적으로만 Session을 만들고 그 요청이 끝나버리면 세션이 다시 사라집니다.
만약에 다른 새로운 요청이 들어오면 그 헤더에 있는 토큰을 통해서 동일한 ID라도 다시 Session을 만들고 그 요청이 끝나면 세션이 사라지는 방식으로 동작합니다.

여기서 JWT하는 방식은 실무에서 유용하게 사용할 수 있는 방식은 아닙니다.
뼈대와 기본 흐름을 알기 위한 방식을 뿐입니다. 나중에 심화 과정을 통해서 정리하겠습니다.






⚒️ 의존성 추가

dependencies {
    implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.12.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5'
}

최신 버전은 0.12.5입니다. 현재 최신 버전은 0.12.6까지 나온 듯 합니다.

🖇️ JWT 최신 정보 버전






⚒️ SpringSecurityConfig 버전 별 구현 방법

SpringSecurityConfig 버전 별 구현 방법
스프링 시큐리티의 경우 세부 버전별로 구현 방법이 많이 다르기 때문에 버전마다 구현 특징을 확인해야 합니다.


스프링 부트 3.1.X
스프링 6.1.X

public class SpringSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((auth) -> auth
                  .requestMatchers("/login", "/join").permitAll()
                  .anyRequest().authenticated()
        );

        return http.build();
    }
}





⚒️ SecurityConfig 클래스 작성

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf((auth) -> auth.disable())
                .formLogin((auth) -> auth.disable())
                .httpBasic((auth) -> auth.disable())
                .authorizeRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll() // 모든 권한 허용
                        .requestMatchers("/admin").hasRole("ADMIN") // ADMIN 권한을 가진 사람만 허용
                        .anyRequest().authenticated()) // 그 외의 다른 요청들은 로그인한 사용자만 접근할 수 있다.
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
                // Session을 STATLESS 하게 만들어야 하기 때문에

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

csrf는 disable로 설정합니다. 세션 방식에서는 세션이 항상 고정되기 때문에 csrf 공격이 필수적으로 방어돼야 합니다. JWT 방식은 Session을 STATLESS 상태로 관리하기 때문에 csrf에 대한 공격을 방어하지 않아도 됩니다.
또 JWT로 진행할 것이기 때문에 formLogin()과 httpBasic() 인증 방식을 disable 합니다.

경로별 인가 작업은 http.authorizeHttpRequests를 통해서 진행할 수 있습니다.
시큐리티를 통해서 회원 정보를 저장하고 회원 가입하고 다시 검증할 때는 항상 Password를 해시로 암호화 시켜서 검증하고 진행하게 됩니다. 그래서 BCryptPasswordEncoder 를 빈으로 등록합니다.






⚒️ DB연결 및 Entity 작성

회원 정보 검증하기 위해서는 ID와 Password를 받고 내부적인 회원 정보로 검증을 진행해야 합니다.

@Entity
@Getter
@Setter
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String username;
    private String password;
    private String role;
}
@Slf4j
@RestController
@ResponseBody
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @PostMapping("/signup")
    public ResponseEntity<SignUpDto> signUp(SignUpDto signUpDto) {
        log.info("Sign Up DTO: {}", signUpDto.getUsername());

        try{
            memberService.signUpProcess(signUpDto);
            return ResponseEntity.ok(signUpDto);
        } catch(Exception e){
            log.error("회원가입 실패: {}", e.getMessage());
            return status(BAD_REQUEST).body(null);
        }
    }

    @GetMapping("/admin")
    public String adminP() {
        return "Admin Controller";
    }

    @GetMapping("/")
    public String homeP() {
        return "Home Controller";
    }
}





⚒️ 회원 가입

POST 요청으로 ID와 Password를 담아서 회원 가입을 위해 던지면 데이터를 DTO로 받아서 MemberController에서 경로에 대한 매핑을 진행해가지고 DTO 데이터를 받습니다.
받아서 MemberService 단으로 넘기면 DTO 데이터를 Member에 옮겨 담아서 MemberRepository 진행 후 DB 내부에 Member 테이블에다가 회원 정보를 저장합니다.

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public void signUpProcess(SignUpDto signUpDto) {
        String username = signUpDto.getUsername();
        String password = signUpDto.getPassword();

        Boolean isExist = memberRepository.existsByUsername(username);

        if(isExist) return;

        Member member = new Member();
        member.setUsername(username);
        member.setPassword(bCryptPasswordEncoder.encode(password));
        member.setRole("ROLE_ADMIN");

        memberRepository.save(member);
    }
}
@Slf4j
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @PostMapping("/admin")
    public ResponseEntity<SignUpDto> signUp(@RequestBody SignUpDto signUpDto) {
        log.info("Sign Up DTO: {}", signUpDto.getUsername());

        try{
            log.error("회원가입 성공: Success");
            memberService.signUpProcess(signUpDto);
            return ResponseEntity.ok(signUpDto);
        } catch(Exception e){
            log.error("회원가입 실패: {}", e.getMessage());
            return status(BAD_REQUEST).body(null);
        }
    }
}





⚒️ 로그인 필터 구현

스프링 시큐리티는 클라이언트의 요청이 여러 개의 필터를 거쳐 DispatcherServlet(Controller)으로 향하는 중간 필터에서 요청을 가로챈 후 검증(인증/인가)를 진행합니다.


서블릿 컨테이너(톰캣)에 존재하는 필터 체인에 DelegatingFilter를 등록한 뒤 모든 요청을 가로챕니다.
가로챈 요청은 SecurityFilterChain에서 처리한 후 상황에 따른 거부, 리다이렉션, 서블릿으로 요청 전달을 진행합니다.






⚒️ 로그인 방식에서 UsernamePasswordAuthenticationFilter

Form 로그인 방식에서는 클라이언트단이 username과 password를 전송한 뒤 Security 필터를 통과하는데 UsernamePasswordAuthenticationFilter에서 회원 검증을 진행을 시작한다.
회원 검증의 경우 UsernamePasswordAuthenticationFilter 가 호출한 AuthenticationManager 를 통해 진행하며 DB에서 조회한 데이터를 UserDetailsService를 통해 받습니다.

JWT에서는 formLogin을 disable 했기 때문에 기본적으로 활성화되어 있는 해당 필터는 동작하지 않습니다. 따라서 로그인을 진행하기 위해서 필터를 커스텀하여 등록해야 합니다.


  • 아이디, 비번 검증을 위한 커스텀 필터 작성(ID와 PW가 형식적으로 유효한지를 확인 -> 입력 특정 규칙)
  • DB에 저장되어 있는 회원 정보를 기반으로 검증할 로직 작성(사용자가 입력한 ID, PW가 DB에 있는 정보와 일치하는지 확인)
  • 로그인 성공시 JWT를 반환할 success 핸들러 생성
  • 커스텀 필터 SecurityConfig에 등록


UsernamePasswordAuthentication 작성

@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        // 클라이언트 요청ㅇ서 username, password 추출
        String username = obtainUsername(req);
        String password = obtainPassword(req);

        // username과 password 검증하기 위해서는 token에 담아야 함.
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
        //token에 담은 검증을 위한 AuthenticationManager로 전달 (검증 진행)
        return authenticationManager.authenticate(authToken);
    }


    /**
     * 검증 성공하면 진행할 메서드
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {

    }
}

UsernamePasswordAuthenticationFilterAuthenticationManager에게 username과 password를 DTO처럼 바구니에 담아서 던져줘야 합니다.
그 바구니가 UsernamePasswordAuthenticationToken 입니다.


.addFilterAt(new LoginFilter(), UsernamePasswordAuthenticationFilter.class); // 추가

이후 만든 필터를 SecurityConfig에 등록해야 합니다.

.addFilterAt : 원하는 자리에 등록.
.addFilterBefore : 해당 하는 필터 전에 등록.
.addFilterAfter : 특정한 필터 이후에 등록.

LoginFilter는 특정한 인자를 받습니다. AuthenticationManager를 주입받았기 때문에 SecurityConfig에도 주입해줘야 합니다. 그래서 빈으로 AuthenticationManager을 반환하는 메서드를 추가합니다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf((auth) -> auth.disable())
                .formLogin((auth) -> auth.disable())
                .httpBasic((auth) -> auth.disable())
                .authorizeRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll() // 모든 권한 허용
                        .requestMatchers("/admin").hasRole("ADMIN") // ADMIN 권한을 가진 사람만 허용
                        .anyRequest().authenticated()) // 그 외의 다른 요청들은 로그인한 사용자만 접근할 수 있다.
                // Session을 STATLESS 하게 만들어야 하기 때문에
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

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

LoginFilter에 인자를 넣어주고 AuthenticationManager 또한 configuration 인자를 받기 때문에 상단에 주입받아서 넣어줍니다.






⚒️ DB기반 로그인 검증 로직

이전까지 한 부분이 로그인 요청 들어오면 username과 password를 꺼내서 검증하는 토큰 객체를 만들고 이것을 AuthenticationManager에게 넘겨주는 작업까지 완료된 것입니다.

뒷 부분은 Session과 동일하게 사용됩니다. DB로부터 UserDetailsService가 특정한 회원 정보를 가져와서 UserDetails로 넘겨서 최종적으로 AuthenticationManager에서 검증을 진행합니다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository; // DB에 접근할 Repository

    /**
     * DB에서 특정 유저를 조회해서 반환해주는 메서드
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username);
        if(member != null) {
            return new CustomUserDetails(member);
        }

        return null;
    }
}

CustomUserDetailsCustomUserDetailsService에서 AuthenticationManager에게 넘겨줄 회원 정보도 커스텀해서 넘겨줘야 합니다. 이것도 DTO에 해당합니다.

package com.example.springessentialguide.data.dto;


import com.example.springessentialguide.data.entity.Member;
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 CustomUserDetails implements UserDetails {
    private final Member member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return member.getRole();
            }
        });
        return collection;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
        // return true
    }

    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
        // return true
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
        // return true
    }

    @Override
    public boolean isEnabled() {
        return UserDetails.super.isEnabled();
        // return true
    }
}

여기까지 하면 로그인이 돌아가고 로그인 성공과 실패에 대한 메서드를 실행하게 됩니다.






⚒️ JWT를 발급하고 검증하는 클래스 만들기 (JWTUtil)

로그인이 성공하면 사용자에게 JWT를 발급해줘야 하고 사용자는 JWT를 가지고 모든 경로에 접근합니다. 접근할 때 JWT가 우리 서버에서 생성됐는지, 유효 기간이 지났는지 등을 검증하는 로직이 필요합니다.
이곳에 발급, 검증하는 메서드를 작성해서 앞으로 필터나 SuccessfulHandler 에서 사용할 수 있도록 컴포넌트를 등록하겠습니다.


Header와 Payload는 단순 BASE64 인코딩만 해서 두기 때문에 외부에서 언제든지 데이터를 디코딩해서 확인할 수 있습니다. 그래서 외부에서 열람해도 되는 정보만을 담아야 합니다.
왜 비번같은 것들을 담지 못하는데 사용하느냐 하면 토큰 자체의 발급처를 확인하기 위해서 사용합니다. 토큰이 신뢰할 수 있는, 나의 서버에서 발급한 것만을 확인하기 위해서 사용합니다.

먼저 지금은 대칭키 암호화 과정으로 진행했습니다. -> HS256 (기초이므로)


JWT를 암호화하고 복호화시킬 비번을 생성.

jwt:
  secret: 9ac1addab338d864812854ea2f73c12687e530951b6521ca89cedc5e75de4736
  • 토큰 Payload에 저장될 정보
    • username
    • role
    • 생성일
    • 만료일
  • JWTUtil 구현 메소드
    • JWTUtil 생성자
    • username 확인 메소드
    • role 확인 메소드
    • 만료일 확인 메소드
@Component
public class JWTUtil {
    private SecretKey secretKey; // 객체 키 생성

    public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
        // 이 키는 특정하게 JWT에서 객체 타입으로 만들어서 저장.-> 키를 암호화 진행
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    /**
     * 검증 진행할 메서드 3개
     * 우리 서버에서 가져온 것이 맞는지 확인.
     */
    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, Long expiredMs) {

        return Jwts.builder()
                .claim("username", username) // claim으로 특정한 키에 대한 데이터를 넣어줌.
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis())) // 토큰이 언제 발생
                .expiration(new Date(System.currentTimeMillis() + expiredMs)) // 만료 기간
                .signWith(secretKey) // 토큰 시그니쳐를 만들어서 암호화 진행.
                .compact(); // 토큰 생성.
    }
}





⚒️ 로그인 성공 JWT 발급

JWTUtil을 주입해놓으면 SecurityConfig 에서 에러가 발생합니다. authenticationManager 인자를 초기화 해줬는데 JWT 인자가 추가되어 SecurityConfig에도 UWTUtil을 주입해주고 .addFilter()에 JWT 인자를 추가해줍니다.

.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);





⚒️ SuccessfulAuthentication 메소드 구현 + fail

@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                        Authentication authentication) throws IOException, ServletException {
    // member 객체를 알아내기 위해 CustomUserDetails
    CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();

    String username = customUserDetails.getUsername();

    // role 값 뽑아내는 방법
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
    GrantedAuthority auth = iterator.next();

    String role = auth.getAuthority();

    String token = jwtUtil.createJwt(username, role, 60*60*10L);

    // HTTP 인증 방식은 RFC 7235 정의에 따라 아래 인증 헤더 형태를 가져야 한다.
    // 예시 - Authorization: Bearer 인증토큰string
    res.addHeader("Authorization", "Bearer " + token);
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
    response.setStatus(401);
}

여기까지 해서 로그인을 진행하면 응답 헤더에 Authorization으로 토큰을 받은 것을 볼 수 있습니다.

앞으로 이 값을 복사해서 어떤 특정한 요청을 보낼 때 헤더에 넣어서 보내면 됩니다.






⚒️ JWT 검증 필터

현재 권한을 가진 사용자에 대한 JWT 검증 필터를 넣어주지 않았기 때문에 "/admin" 에는 요청을 보낼 수가 없습니다.
토큰을 검증해서 내부에 STATLESS지만 한 번의 요청에 대해서 Session을 만드는 토큰 검증 필터를 생성하겠습니다.
Session은 STATLESS 이므로 해당 요청이 끝나면 소멸됩니다. 동일한 요청을 다시 보내면 다시 토큰 검증합니다.

이 필터를 구현하고 구현한 필터를 등록만 시켜주면 JWT 기능적인 부분은 끝납니다.

@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
    private final JWTUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // JWT를 request 해서 검증을 진행하는데 그러기 위해서는 JWTUtil을 통해서 필터를 검증할 메서드를 가져와야 한다.
        String authorization = request.getHeader("Authorization");
        if(authorization == null && !authorization.startsWith("Bearer ")) {
            log.error("Authorization header is missing");
            filterChain.doFilter(request, response); // chain 방식으로 엮여있는 필터들에서 이 필터를 종료하고 다음 필터로 넘겨준다.
            return;
        }

        String token = authorization.split(" ")[1]; // 즉 "Bearer " 이후 부분
        if (jwtUtil.isExpired(token)) {
            log.error("JWT Token expired");
            filterChain.doFilter(request, response);
            return; // 메서드 종료
        }

        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);
        Member member = new Member();
        member.setUsername(username);
        member.setPassword("temppassword"); // 비번의 경우 토큰에 들어가지 않기 때문에 임시로 넣어서 초기화.
        member.setRole(role);

        // UserDetails에 회원 정보 객체 담기
        CustomUserDetails customUserDetails = new CustomUserDetails(member);
        // 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

		// 세션에 사용자 등록 -> 이 authToken을 SecurityContextHolder에 넣으면 이 요청에 대해서 유저 세션을 생성할 수 있습니다.
        SecurityContextHolder.getContext().setAuthentication(authToken);
        filterChain.doFilter(request, response);
    }
}

비번의 경우 토큰에 들어가지 않기 때문에 임시로 넣어서 초기화를 했는데 DB에서 조회를 하면 매번 요청이 올 때마다 DB를 조회하는 좋지 않은 상황이 발생하기 때문에 ContextHolder에 정확한 비번을 넣을 필요가 없습니다.

세션까지 만들었으니 JWT 검증하는 필터가 끝이 났고 이 필터를 SecurityConfig에 등록해서 시큐리티가 동작할 때 필터도 동작할 수 있도록 등록해야 합니다.






⚒️ CORS

백에서 하는 방법

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		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;
                }
            })));

    return http.build();
}

Postman 사용
/login에서 응답 헤더로 나온 Bearer Token을 복사해서 GET로 보내려는 /admin에다가 요청 Headers에 Authorization 으로 붙여넣고 Send하면 됩니다.






Reference

🖇️ https://www.devyummi.com/page?id=668e56d19c2abb62ff0b2987

profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글