스프링 공부/기타

Spring Security (멋쟁이사자처럼 영상강의) 실습 기록

모항 2023. 7. 15. 05:14

팀 프로젝트에서 회원가입,로그인 파트를 맡게 되었다.

이를 위해 Spring Security를 배워야 해서 구글링을 하다가, 다음의 플레이리스트를 발견했다.

 

https://www.youtube.com/playlist?list=PLAdQRRy4vtQTJawYfraUTUf6rCfWFYqKj

 

멋쟁이 사자처럼 백엔드

 

www.youtube.com

 

우리 팀 프로젝트와 버전 차이가 크지 않아 나에게 딱 맞는 강의였다. 강의의 내용도 좋았다.

 

이 게시글은 이 플레이리스트의 40번 강의부터 52번 강의까지를 들으며 진행한 실습 내용 기록이다.

 

 


프로젝트 생성

 

Spring Initializr (https://start.spring.io/) 를 이용하여 다음과 같은 프로젝트를 생성하였다.

 

일부러 강의가 아닌 우리 팀 프로젝트와 동일한 세팅으로 진행하였다.

Spring Security와 JWT 관련 디펜던시는 후에 build.gradle 수정을 통해 추가할 것이다.

 

 

 

application.yml은 다음과 같이 작성했다.

server:
  servlet:
    encoding:
      force-reponse: true

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/데이터베이스명?createDatabaseIfNotExist=true&characterEncoding=UTF-8&characterSetResults=UTF-8
    username: root
    password: 내 로컬 비밀번호
  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: true
    show-sql: true

 

 

 

깃 설정도 해주었다.

 

https://github.com/00blowup/2023SpringSecurityDemo

 

GitHub - 00blowup/2023SpringSecurityDemo

Contribute to 00blowup/2023SpringSecurityDemo development by creating an account on GitHub.

github.com

 

 


회원가입 기능 구현

 

Security를 적용하기 전에, 기본적인 회원가입 기능부터 빠르게 구현해보자.

 

userName과 password를 받아와서,

userName이 중복되는지 체크한 후,

중복되지 않을 경우 DB에 회원정보를 새롭게 저장한다.

 

이를 위해 만든 controller, domain, DTO, repository, service의 파일 위치는 다음과 같고

내용은 각각 다음과 같다.

 

UserController.java

package hello.securitydemo.controller;

import hello.securitydemo.domain.dto.UserJoinRequestDto;
import hello.securitydemo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserService userService;

    @PostMapping("/join")
    public ResponseEntity<String> join (@RequestBody UserJoinRequestDto requestDto) {
        userService.join(requestDto.getUserName(), requestDto.getPassword());
        return ResponseEntity.ok().body("successfully joined");
    }
}

 

UserJoinRequestDto.java

package hello.securitydemo.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class UserJoinRequestDto {
    private String userName;
    private String password;
}

 

User.java

package hello.securitydemo.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class User {

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

    private String userName;

    private String password;

}

 

UserRepository.java

package hello.securitydemo.repository;

import hello.securitydemo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUserName(String userName);
}

 

UserService.java

package hello.securitydemo.service;

import hello.securitydemo.domain.User;
import hello.securitydemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public String join(String userName, String password) {
        // userName 중복 체크
        userRepository.findByUserName(userName)
                .ifPresent(user -> {
                    throw new RuntimeException(userName + " already exists!");
                });
        // 이미 있는 유저명일 경우 예외 발생

        // 예외가 없었다면 저장 수행
        userRepository.save(
                User.builder()
                        .userName(userName)
                        .password(password)
                        .build()
        );

        return "SUCCESS";
    }
}

 

 

 

다 만들었으니 테스트를 해보자.

 

강의에서 안내한 테스트코드 작성 대신, Postman을 이용해 직접 요청을 보내보는 방식으로 테스트하겠다.

 

Application을 실행시키고, 아래와 같이 요청을 보내보면, 회원가입이 잘 되었다는 응답이 온다.

 

 

MySQL Workbench를 켜 데이터베이스를 확인해보면, 데이터가 잘 들어가있음이 보인다.

 

 

 


 

유저명이 중복될 때 예외가 잘 던져지는지도 확인하자.

 

방금 회원가입한 정보로 다시 가입을 시도하면, 다음과 같이 에러가 발생한다.

 

 

콘솔을 확인해보면 우리가 의도한 대로 예외가 잘 던져졌음을 알 수 있다.

 

 


Spring Security 추가

이제 Spring Security를 추가해주자.

처음에 추가하지 않았던 Spring Security 디펜던시를 지금 추가해준다.

 

build.gradle의 디펜던시 코드에 다음의 코드를 추가하고 다시 빌드해주면 된다.

 

implementation 'org.springframework.boot:spring-boot-starter-security'

 

IntelliJ 하단의 디펜던시 검색창에서 Spring Security를 검색하여 추가해도 된다.

IntelliJ 하단의 검색창. Add를 눌러 디펜던시를 추가할 수 있다.

 

 

이렇게 Spring Security 라이브러리를 추가하고 나면, 이전에는 잘 되었던 join이 막혀 동작하지 않게 된다.

 

 

위와 같이 401 Unauthorized 에러가 발생한다.

 

이제 Security 설정 코드를 작성하여 다시 권한을 부여해주자.

아래와 같이 configuration 패키지를 만들고 SecurityConfig 클래스를 생성한다.

 

 

SecurityConfig.java를 먼저 다음과 같이 작성한다.

모든 POST 요청에 기본적으로 인증을 요구하고, "/api/v1/"로 시작하는 모든 URI에도 인증을 요구하지만,

인증을 받지 않은 상태인 것이 당연한 회원가입 및 로그인은 무조건 허용으로 설정하는 코드이다.

package hello.securitydemo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain (HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll()    // 회원가입, 로그인은 무조건 허용
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated() // /api/로 시작하는 다른 URI의 POST 요청은 모두 인증 요구
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT를 사용하므로 stateless
                .and()
                .build();
    }
}

 

이렇게 해주었으면 "api/v1/users/join"의 접근이 허용되어 다시 회원가입이 정상적으로 작동할 것이다.

한 번 Postman으로 확인해보자.

 

방금은 안 되었던 회원가입이 지금은 잘 된다.

 

DB에도 잘 추가되었다.

 


비밀번호 인코딩

현재 DB에는 비밀번호가 그대로 저장되어있다.

즉, DB을 볼 수 있는 사람이라면 누구나 모든 유저의 비밀번호를 그대로 빼내갈 수 있다는 뜻이다.

그러므로 비밀번호를 암호화하여 DB에 저장하도록 하자.

 

configuration 패키지에 EncoderConfig 클래스를 추가한다.

내용은 다음과 같이 채운다.

package hello.securitydemo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class EncoderConfig {

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

 

이제 이 EncoderConfig를 이용해 비밀번호를 인코딩하도록 UserService의 내용을 수정해준다.

 

새 필드로

private final BCryptPasswordEncoder encoder;

를 추가해주고,

 

원래 아래와 같이 password를 그대로 저장하던 코드를

.password(password)

이래처럼 인코딩하여 저장하도록 바꾼다.

.password(encoder.encode(password))

 

전체 코드는 이렇다.

package hello.securitydemo.service;

import hello.securitydemo.domain.User;
import hello.securitydemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;	// 필드 추가

    public String join(String userName, String password) {
        // userName 중복 체크
        userRepository.findByUserName(userName)
                .ifPresent(user -> {
                    throw new RuntimeException(userName + " already exists!");
                });
        // 이미 있는 유저명일 경우 예외 발생

        // 예외가 없었다면 저장 수행
        userRepository.save(
                User.builder()
                        .userName(userName)
                        .password(encoder.encode(password)) // password -> encoder.encode(password)
                        .build()
        );

        return "SUCCESS";
    }
}

 

이제 비밀번호는 암호화되어 DB에 저장될 것이다.

Postman을 통해 확인해보자.

Application을 실행시키고 아래와 같은 요청을 보낸다.

Postman 상으로는 별 차이가 없어보이지만, DB를 확인해보면 다르다.

 

 

분명 1번 & 2번 회원과 동일하게 "test1"이라는 password로 회원가입하였으나 저장된 내용은 암호화되어있다.

 

 


로그인 기능 구현

이제 로그인 기능을 구현한다.

아이디와 비밀번호를 전달받으면 아래 3가지 중 하나의 일이 일어난다.

  1. 로그인에 성공하여 토큰이 발행된다.
  2. 존재하지 않는 ID를 입력하여 로그인에 실패하고 에러가 발생한다.
  3. 존재하는 ID와 잘못된 비밀번호를 입력하여 로그인에 실패하고 에러가 발생한다.

이러한 상황에 맞추어 개발을 해보자.

 

먼저 JWT 디펜던시를 추가해준다.

build.gradle의 dependency 코드에 다음을 추가한다.

implementation 'io.jsonwebtoken:jjwt:0.9.1'

혹은 강사님처럼 다음과 같이 추가해도 된다. 효과는 똑같다.

 

주의할 점이 있다. 디펜던시 검색창에서 jjwt를 검색하면 jjwt 최신 버전이 표시되는데, 이것을 추가하면 안 된다.

IntelliJ 하단의 검색창. Add를 눌러 디펜던시를 추가할 수 있다.

이 강의의 코드대로 개발하려면 jjwt-root의 0.11.5 버전이 아닌, jjwt의 0.9.1 버전을 사용해야 한다.

 

추가했으면 다시 빌드하여 적용해준다.

 

 

이제 코드를 작성하자.

 

utils 패키지를 새로 생성하고 JwtTokenUtil 클래스를 만든다.

 

내용은 다음과 같이 채운다.

package hello.securitydemo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtTokenUtil {
    public static String createToken (String userName, String key, long expireTimeMs) {
        Claims claims = Jwts.claims();  // 일종의 Map이다
        claims.put("userName", userName);   // 회원명을 저장

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))  // 발행 시각
                .setExpiration(new Date(System.currentTimeMillis() + expireTimeMs)) // 만료 시각
                .signWith(SignatureAlgorithm.HS256, key)    // HS256이라는 알고리즘과 주어진 key를 이용해 암호화
                .compact()
                ;

    }
}

 

 

UserController.java에 다음의 함수를 추가한다.

@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody UserLoginRequestDto requestDto) {
    String token = userService.login(requestDto.getUserName(), requestDto.getPassword());
    return ResponseEntity.ok().body("successfully logged in (token: " + token + ")");
}

 

유저로부터 userName과 password를 받아오는 UserLoginRequestDto 클래스를 domain.dto 패키지에 추가하고 다음과 같이 작성한다.

package hello.securitydemo.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class UserLoginRequestDto {
    private String userName;
    private String password;
}

 

UserService에는 다음과 같이

필드 2개를 추가하고

private Long expireTimeMs = 1000 * 60 * 60L;    // 1시간

@Value("${jwt.key}")
private String key;

login 함수를 추가한다.

public String login(String userName, String password) {
    // 존재하지 않는 회원명을 입력한 상황의 예외처리
    User selectedUser = userRepository.findByUserName(userName)
            .orElseThrow(() -> new RuntimeException("username not found!"));

    // 잘못된 비밀번호를 입력한 상황의 예외처리
    if(!encoder.matches(password, selectedUser.getPassword())) throw new RuntimeException("wrong password!");

    // 로그인 성공
    String token = JwtTokenUtil.createToken(selectedUser.getUserName(), key, expireTimeMs);

    // 리턴
    return token;

}

 

이때 잘못된 비밀번호를 입력한 상황의 예외처리 조건문은 다음과 같이 equals()를 사용해 적으면 안 된다.

if(!selectedUser.getPassword().equals(password)) throw new RuntimeException("wrong password!");

 

암호화된 데이터와 그렇지 않은 데이터를 비교하는 것이기 때문에, 다음과 같이 encoder를 사용해서 비교해야 한다.

if(!encoder.matches(password, selectedUser.getPassword())) throw new RuntimeException("wrong password!");

이때 password와 selectedUser.getPassword()의 순서가 절대 바뀌어서는 안 된다! 첫 순서의 문자열을 인코딩해서 두 번째 순서의 문자열과 비교하기 때문이다.

 

 

또한 여기서 문자열 변수 key의 값은 @Value 어노테이션에 의해 결정된다.

@Value는 properties 혹은 yml 파일에서 값을 가져온다.

이 프로젝트에서 @Value("${jwt.key}") 는 application.yml의 jwt: key: 이하에 있는 값을 가져오겠다는 의미이다.

따라서 applcation.yml 파일에 다음과 같은 코드를 추가한다.

jwt:
  key: 아무 문자열이나 적기. ""는 있든 없든 상관 없음.

 

 

 

 

 

Postman으로 테스트해보자.

새로 회원가입을 하나 하고,

이 정보로 로그인을 시도한다.

 

 

Token 발행이 잘 되는 것이 보인다.

 

 


권한 부여

Token만 부여한다고 끝이 아니다. Token을 부여한 것은 결국 각 사용자에게 권한을 부여하고, 그 권한을 확인하기 위해서다.

 

권한을 부여하는 코드를 작성해보자.


권한을 부여하기 위해서는 Filter를 사용해야 한다. 이를 위해, SecurityConfig.java에 Filter를 사용한다는 코드를 추가해주자.

package hello.securitydemo.configuration;

import hello.securitydemo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserService userService;
    @Value("${jwt.key}")
    private String secretKey;

    @Bean
    public SecurityFilterChain securityFilterChain (HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll()
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(new JwtFilter(secretKey), UsernamePasswordAuthenticationFilter.class)	// 추가된 한 줄
                .build();
    }
}

 

다음으로, configuration 패키지에 JwtFilter.java 클래스를 생성한 뒤 아래와 같이 코드를 작성한다.

package hello.securitydemo.configuration;

import hello.securitydemo.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final UserService userService;
    private final String secretKey;

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

        String userName = "";
        
        // 권한 부여하기
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userName, null, List.of(new SimpleGrantedAuthority("USER")));
        // Detail 넣기
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

 

 

 

토큰의 유효성 검사

다음으로, 토큰의 유효성을 검사해야 한다.

지금은 토큰의 값을 까보지도 않고 무조건 다 USER 권한을 부여하도록 되어있다.

 

토큰의 값이 null은 아닌지, 토큰의 내용이 이상하지는 않은지, 토큰의 유효기간이 끝나지는 않았는지 체크하는 코드를 추가하자.

 

JwtFilter.java를 다음과 같이 수정한다.

package hello.securitydemo.configuration;

import hello.securitydemo.service.UserService;
import hello.securitydemo.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 헤더에서 token 가져오기
        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization: {}", authorization);

        // token이 null이 아닌지, Bearer 로 시작하는지 확인
        if(authorization == null || !authorization.startsWith("Bearer ")) {
            log.error("inappropriate authentication!");
            filterChain.doFilter(request, response);
            return;
        }

        // token에서 Bearer 떼어내서 token만 남기기
        String token = authorization.split(" ")[1]; // authorization의 두 번째 단어

        // token이 expire되었는지 확인
        if(JwtTokenUtil.isExpired(token, secretKey)) {
            log.error("token is expired!");
            filterChain.doFilter(request, response);
            return;
        }

        String userName = "";

        // 권한 부여하기
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userName, null, List.of(new SimpleGrantedAuthority("USER")));
        // Detail 넣기
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

 

위 코드를 잘 보면, token이 expire되었는지 확인하는 부분에서 JwtTokenUtil.isExpired()라는 함수가 사용되었음을 알 수 있다.

이 함수를 JwtTokenUtil.java 내에 선언해주자.

다음과 같은 코드를 JwtTokenUtil.java에 추가하면 된다.

// token이 expire되었으면 true를 리턴하는 함수
public static boolean isExpired(String token, String secretKey) {
    return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
            .getBody().getExpiration().before(new Date());
}

 

 

Token에서 userName 꺼내어 사용하기

마지막으로, Token에서 userName 값을 꺼내는 기능을 추가한다.

 

JwtFilter.java를 다음과 같이 수정한다. ("token에서 userName 꺼내기"라는 주석이 달린 단락 부분만 수정하면 된다)

package hello.securitydemo.configuration;

import hello.securitydemo.service.UserService;
import hello.securitydemo.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 헤더에서 token 가져오기
        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization: {}", authorization);

        // token이 null이 아닌지, Bearer 로 시작하는지 확인
        if(authorization == null || !authorization.startsWith("Bearer ")) {
            log.error("inappropriate authentication!");
            filterChain.doFilter(request, response);
            return;
        }

        // token에서 Bearer 떼어내서 token만 남기기
        String token = authorization.split(" ")[1]; // authorization의 두 번째 단어

        // token이 expire되었는지 확인
        if(JwtTokenUtil.isExpired(token, secretKey)) {
            log.error("token is expired!");
            filterChain.doFilter(request, response);
            return;
        }

        // token에서 userName 꺼내기
        String userName = JwtTokenUtil.getUserName(token, secretKey);
        log.info("userName:{}", userName);

        // 권한 부여하기
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userName, null, List.of(new SimpleGrantedAuthority("USER")));
        // Detail 넣기
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

expire되었는지 확인할 때와 비슷하게 여기서는 JwtTokenUtil.getUserName() 함수가 사용되었다.

JwtTokenUtil.java에 아래의 함수를 추가한다.

// token에서 userName을 꺼내어 리턴하는 함수
public static String getUserName(String token, String secretKey) {
    return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
            .getBody().get("userName", String.class);
}

 

여기까지 하면 완성이다.

 

이제 테스트를 진행해보자.

 

나는 현재 로그인되어있는 유저의 token이 유효할 경우 userName의 값을 응답으로 보내는 코드를 작성하여 테스트를 Postman을 통해 진행할 것이다.

 

아래와 같은 함수 하나를 UserController.java에 추가해준다.

@PostMapping("/authtest")
public ResponseEntity<String> authtest (Authentication authentication) {
    return ResponseEntity.ok().body("userName: " + authentication.getName());
}

 

이제 Postman을 열어 로그인을 해주고,

응답으로 돌아온 token 값을 복사하여 auth 테스트에 사용한다.

 

이렇게 복사해두고,

 

아래처럼 Content-Type 헤더에는 application/json을,

Authorization 헤더에는 "Bearer (복사한 토큰)"을 넣어 테스트해주면 된다.

 

회원명이 정확하게 응답된 것을 확인할 수 있다.