Day_40 ( 스프링 - 7 )

HD.Y·2023년 12월 23일
0

한화시스템 BEYOND SW

목록 보기
35/58
post-thumbnail

🐻 세션 기반 인증 웹페이지 테스트

  • Day_38 에서 간단하게 강사님이 주신 프론트엔드 서버를 실행하여, 회원가입 및 로그인, 상품주문 및 주문내역 조회를 실습해봤었다.

  • 하지만, 그때는 로그인 기능을 배우지 않아서 그냥 Read 를 로그인 하는거라고 가정하고 실습했었는데, 이제는 세션 기반 인증을 배웠기 때문에 실제로 적용을 해볼 수 있었다.

  • 지난시간에 배운 것을 토대로 Member 에 대한 클래스들을 수정한 내용을 아래에 적어보겠다.

    Member 엔티티 클래스

    @Entity
    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Member implements UserDetails {
    
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       private Integer id;
    
       @JsonProperty("email")
       @Column(name = "email")
       private String username;
    
       private String password;
       private String authority;
    
       @OneToMany(mappedBy = "member")
       private List<Orders> orderList = new ArrayList<>();
    
       @Override
       public Collection<? extends GrantedAuthority> getAuthorities() {
           return Collections.singleton((GrantedAuthority) () -> authority);
       }
    
       @Override
       public boolean isAccountNonExpired() {
           return true;
       }
    
       @Override
       public boolean isAccountNonLocked() {
           return true;
       }
    
       @Override
       public boolean isCredentialsNonExpired() {
           return true;
       }
    
       @Override
       public boolean isEnabled() {
           return true;
       }
    }

    MemberRepository 인터페이스

    @Repository
    public interface MemberRepository extends JpaRepository<Member, Integer> {
       public Optional<Member> findByUsername(String username);
    }

    MemberLoginReq / Res 클래스

    // MemberLoginReq
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class MemberLoginReq {
       @JsonProperty("email")
       private String username;
       private String password;
    }
    
    // MemberLoginRes
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class MemberLoginRes {
       private Integer id;
    
       @JsonProperty("email")
       private String username;
    
       private String password;
    }

    MemberService 클래스

    @Service
    public class MemberService implements UserDetailsService {
    
       private final MemberRepository memberRepository;
       private final PasswordEncoder passwordEncoder;
    
       public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
           this.memberRepository = memberRepository;
           this.passwordEncoder = passwordEncoder;
       }
    
       // Signup
       public void signup(MemberLoginReq memberLoginReq) {
           if(!memberRepository.findByUsername(memberLoginReq.getUsername()).isPresent())
           memberRepository.save(Member.builder()
                           .username(memberLoginReq.getUsername())
                           .password(passwordEncoder.encode(memberLoginReq.getPassword()))
                           .authority("ROLE_USER")
                   .build());
       }
    
       @Override
       public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
           Optional<Member> result = memberRepository.findByUsername(username);
           Member member = null;
           if(result.isPresent()) {
               member = result.get();
           }
           return member;
       }
    }

    MemberController 클래스

    @RestController
    @RequestMapping("/member")
    public class MemberController {
    
       private final MemberService memberService;
       private final AuthenticationManager authenticationManager;
    
       public MemberController(MemberService memberService, AuthenticationManager authenticationManager) {
           this.memberService = memberService;
           this.authenticationManager = authenticationManager;
       }
    
       @RequestMapping(method = RequestMethod.POST, value = "/signup")
       public ResponseEntity signup(@RequestBody MemberLoginReq memberLoginReq) {
           memberService.signup(memberLoginReq);
    
           return ResponseEntity.ok().body("ok");
       }
    
       @RequestMapping(method = RequestMethod.POST, value = "/login")
       public ResponseEntity login(@RequestBody MemberLoginReq memberLoginReq){
           Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(memberLoginReq.getUsername(), memberLoginReq.getPassword()));
           SecurityContextHolder.getContext().setAuthentication(authentication);
    
           Member member = ((Member)authentication.getPrincipal());
           return ResponseEntity.ok().body(MemberLoginRes.builder()
                       .id(member.getId())
                       .username(member.getUsername())
               .build());
       }
    }

    SecurityConfig / PasswordEncoderConfig 클래스

    // SecurityConfig
    @Configuration
    public class SecurityConfig {
    
       @Bean
       public AuthenticationManager authenticationManager(
               AuthenticationConfiguration configuration) throws Exception {
           return configuration.getAuthenticationManager();
       }
    
       @Bean
       public SecurityFilterChain securityFilterChain(HttpSecurity http) {
           try {
    
               http.csrf().disable()
                       .authorizeHttpRequests()
                           .antMatchers("/member/login", "/member/signup", "/product/list", "/product/read", "/product/create").permitAll()
                           .antMatchers("/order/create", "/order/list").hasRole("USER")
                           .anyRequest().authenticated()
                       .and()
                           .formLogin().disable();
    
               return  http.build();
           } catch (Exception e) {
               throw new RuntimeException(e);
           }
       }
    }
    
    // PasswordEncoderConfig
    @Configuration
    public class PasswordEncoderConfig {
    
       @Bean
       public PasswordEncoder passwordEncoder() {
           return PasswordEncoderFactories.createDelegatingPasswordEncoder();
       }
    }

  • 이제 웹페이지에 들어가서 회원가입을 하면 아래와 같이 암호화된 비밀번호 및 권한이 부여되어 회원 정보가 저장되는 것을 볼 수있다.

  • 다음으로, 나머지 기능들은 다 지난 실습때 실시해서 똑같이 동작하는데, 2가지 부분을 수정하면서 깨달은게 있었다.

    1) 프론트엔드 서버를 강사님께서 만드실때 상품 주문을 하면 무조건 Member id가 1로
      주문되도록 설정
    이 되어있어서 2번 사용자가 주문을 해도 주문내역엔
      1번 사용자가 주문했다고 들어갔다. 물론 프론트엔드 서버를 수정하면 되겠지만, 나는
      백엔드 개발자로써 백엔드 서버적으로 처리를 어떻게 해줄지에 대한 고민을 실시했다.

    ➡ 그결과, 주문을 생성할때 현재 로그인한 사용자의 정보를 불러와서 그 사람의 id로
       주문을 생성토록 하는것이 가능하다는 것을 알아냈다.

    // ✅ OrderController 클래스
     @RequestMapping(method = RequestMethod.POST, value = "/create")
      public ResponseEntity create(@RequestBody PostOrderReq postOrderReq) {
    
          Member member = ((Member)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
          ordersService.create(member.getId(), postOrderReq);
    
          return ResponseEntity.ok().body("ok");
      }

    ➡ 여기서 스프링 부트의 놀라움을 다시한번 깨달았다. 인증된 사용자의 정보들은 스프링
       시큐리티에서 SecurityContextHolder 안에 SecurityContext 에 Authentication 으로
       저장되는데, 현재 로그인한 사용자의 정보를 아래와 같이 적어서 불러오면,
       SecurityContextHolder.getContext().getAuthentication().getPrincipal()
       몇번 사용자의 정보를 불러와달라 같은 특정 번호의 데이터나 이름이 없이
       불러와진다는 것이다. 이 부분이 정말 신기한게, 자동으로 지금 로그인한 사용자의
       정보를 알아서 세션에서 찾아서 온다는 것이 놀라울 따름이었다.

    2) 위와 동일한 내용일 수 있지만, 주문내역을 조회할 때 로그인 사용자가 주문한 내역만
      볼 수 있도록 설정하는게 필요했다. 다른 사람이 주문한 목록을 보여주면 안되기
      때문이다. 이것도 역시나 현재 로그인한 사용자의 정보를 불러와서 해당 id와 같은
      주문목록만 반환시켜주면 되는 문제여서 쉽게 구현하였다.

    // ✅ OrderService 클래스
        public List<OrdersDto> list(){
          Member member = ((Member)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
          Integer nowUser = member.getId();
          List<Orders> result = ordersRepository.findAll();
    
          List<OrdersDto> ordersDtos = new ArrayList<>();
    
          for(Orders orders : result) {
              Member member2 = orders.getMember();
              Product product = orders.getProduct();
    
              if(member2.getId() == nowUser) {
                  MemberLoginRes memberLoginRes = MemberLoginRes.builder()
                          .id(member.getId())
                          .username(member.getUsername())
                          .build();
    
                  ProductReadRes productReadRes = ProductReadRes.builder()
                          .id(product.getId())
                          .name(product.getName())
                          .price(product.getPrice())
                          .build();
                  OrdersDto ordersDto = OrdersDto.builder()
                          .id(orders.getId())
                          .memberLoginRes(memberLoginRes)
                          .productReadRes(productReadRes)
                          .build();
    
                  ordersDtos.add(ordersDto);
              }
          }
          return ordersDtos;
      }

  • 이렇게 하면 이제 로그인 한 사용자의 주문목록만 아래와 같이 볼 수 있게 된다.


JWT(Java Web Token) 구조와 인증 절차

  • 지난 글에서 세션과 쿠키, 토큰에 대해서 알아보고, 세션 기반 인증을 코드로 구현했다면 이번에는 토큰 기반 인증을 구현해보는 시간을 가졌다.
  • JWT 토큰의 구조는 아래와 같다.

    1) 헤더 : olg ➡ 서명 암호화 알고리즘 / typ ➡ 토큰 유형
    2) 내용 : 토큰에서 사용할 정보의 조각들인 Claim 이 담겨있다.
    3) 서명 : 서명은 헤더와 페이로드를 서버에서 가지고 있는 Key 로 합친 것을 암호화한
         내용을 담고 있다.

  • JWT를 이용한 인증 과정은 아래와 같다.

    1) 사용자가 ID, PW를 입력하여 서버에 로그인 인증을 요청한다.

    2) 서버에서 클라이언트로부터 인증 요청을 받으면, Header, PayLoad, Signature를
      정의한다. Hedaer, PayLoad, Signature를 각각 Base64로 한 번 더 암호화하여 JWT를
      생성하고 이를 클라이언트에게 발급한다.

    3) 클라이언트는 서버로부터 받은 JWT를 로컬 스토리지에 저장한 다음, API를 서버에
      요청할때 Authorization headerAccess Token을 담아서 보낸다.

    4) 서버는 클라이언트가 Header에 담아서 보낸 JWT가 내 서버에서 발행한 토큰인지
      일치 여부를 확인하여 일치한다면 인증을 통과시켜주고 아니면 통콰시키지 않는다.
      인증이 통과되면, 페이로드에 들어있는 유저의 정보들을 select해서 클라이언트에
      돌려준다.

    5) 클라이언트가 서버에 요청을 했는데, 만일 Access Token의 시간이 만료되면
      클라이언트는 Refresh Token 을 이용해서 서버로부터 새로운 Access Token을 발급
      받는다.


🦁 JWT 토큰 구현하기 ( 배운 범위까지 )

  • 스프링 시큐리티는 기본적으로 세션 기반 인증을 사용하기 때문에, JWT 토큰을 구현하기 위해서는 커스텀 필터(스프링 시큐리티가 제공하는 인증필터와 인증매니저 대신 별도의 인증처리를 담당할 커스텀 클래스)JWT 필터를 별도로 만들어 줘야 한다.

  • 먼저 JWT 를 만들기 위해서 라이브러리를 추가해줘야 된다. 추가해줘야될 라이브러리는 아래와 같다. ( 버전은 자바 11, 스프링 2.7.13 기준 적용 가능한 버전이다. )

               <dependency>
    				<groupId>io.jsonwebtoken</groupId>
    				<artifactId>jjwt-impl</artifactId>
    				<version>0.11.5</version>
    
    			</dependency>
    		
                <dependency>
    				<groupId>io.jsonwebtoken</groupId>
    				<artifactId>jjwt-api</artifactId>
    				<version>0.11.5</version>
    
    			</dependency>
          
    			<dependency>
    				<groupId>io.jsonwebtoken</groupId>
    				<artifactId>jjwt-jackson</artifactId>
    				<version>0.11.5</version>
    			</dependency>
  • 다음으로 application.yml 파일에 jwt 관련 설정을 해주는데, 보통 서버가 가지고 있는 secretKey 와 토큰의 만료기한을 설정해 놓는다.
    ➡ 이러한 설정을 여기다 해주는 이유는, 키와 만료기한을 바꾸게 될 떄 설정 파일에서만
      바꾸면 다른 클래스들에 전부 적용이 되기 때문이다. 그렇지 않으면 클래스마다
      필요한 곳에 생성해서 작성해야되는데, 그러면 수정 시 오류가 발생할 가능성도 높을
      것이다.
    ➡ 설정할때는 클래스에서 사용하는 변수 이름과 똑같이 설정하는데, 다만 여기에는
      스네이크 표기법을 사용해야 한다. 나는 아래와 같이 설정해줬다.

     jwt:
    	secret-key: abcdefghijklmnopqrstuvwxyz0123456789
    	token:
          expired-time-ms: 30000

    ➡ 이렇게 설정한 값을 클래스에서 사용하려면 변수명 위에 아래와 같이 어노테이션을
      달아 주면 된다.

     @Value("${jwt.secret-key}")
      private String secretKey;
    
      @Value("${jwt.token.expired-time-ms}")
      private Integer expiredTimeMs;
  • 이렇게 하면 기본 설정은 끝났고, 이제 Access 토큰 발급 및 토큰이 유효한 토큰인지 검증하는 메서드를 만들면 되는데, 이것을 JwtUtils 란 클래스로 생성하여 만들었다.
    이 클래스에 JWT 토큰과 관련된 기능들을 추가하면 되며, 나는 아래와 같이 추가하였다.

public class JwtUtils {

    // Access 토큰 생성
    public static String generateAccessToken(String username, String key, int expiredTimeMs) {

        Claims claims = Jwts.claims();
        claims.put("email", username);  // 토큰의 정보에 입력받은 email 추가

        byte[] secretBytes = key.getBytes();

		// 토큰 생성
        String token = Jwts.builder()
                // JWT에 추가할 클레임들을 설정
                .setClaims(claims)
                // 토큰이 발급된 시간 설정
                .setIssuedAt(new Date(System.currentTimeMillis()))
                // 토큰의 만료 시간 설정
                .setExpiration(new Date(System.currentTimeMillis() + expiredTimeMs))
                // HMAC-SHA256 알고리즘을 사용하여 서명 (토큰이 변조되지 않았음을 검증 하기 위함)
                .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
                // 최종적으로 JWT를 생성하고 문자열로 변환
                .compact();

        return token;
    }

    // 토큰 검증
    public static Boolean validate(String token, String username, String key) {
		
        // 토큰에서 username을 뽑아냄(사용자가 입력한 email ID / email을 ID로 설정해놈)
        String usernameByToken = getUsername(token, key);
		
        // 토큰의 만료시간을 가져옴
        Date expireTime = extractAllClaims(token, key).getExpiration();
        // 토큰의 만료 시간과 현재 시간을 비교한 결과 출력 
        // ( 만료시간이 현재 시간보다 이후라면 false 반환 )
        Boolean result = expireTime.before(new Date(System.currentTimeMillis()));
		
        // 사용자 ID 가 일치하고, 만료 시간이 지나지 않았으면 true 를 반환
        return usernameByToken.equals(username) && !result;
    }

    // 키 변환 메서드 (바이트 단위)
    public static Key getSignKey(String secretKey) {
        return Keys.hmacShaKeyFor(secretKey.getBytes());
    }

    // 사용자 이름을 가져오는 메서드
    public static String getUsername(String token, String key) {
        return extractAllClaims(token, key).get("email", String.class);
    }

    // 토근에 담겨있는 정보(Claims)를 가져오는 메서드
    public static Claims extractAllClaims(String token, String key) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(key.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

  • 다음은 JwtFilter 클래스를 작성한 내용이다.

    public class JwtFilter extends OncePerRequestFilter {
    
      // DB에 저장된 ID와 비교하기 위해 MemberRepository 객체 생성
      private MemberRepository memberRepository;
      
      @Value("${jwt.secret-key}")
      private String secretKey;
    	
      // 의존성 주입
      public JwtFilter(String secretKey, MemberRepository memberRepository) {
          this.secretKey = secretKey;
          this.memberRepository = memberRepository;
      }
    
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          // HTTP 요청 헤더에서 "Authorization" 헤더 값을 가져옴
          String header = request.getHeader(HttpHeaders.AUTHORIZATION);
    
          String token;
          // 헤더가 null이 아니고, Bearer 로 시작한다면
          if (header!= null && header.startsWith("Bearer ")) {
              // 헤더에서 토큰을 추출
              token = header.split(" ")[1];
          } else {
              // 그렇지 않으면 다음 필터로 이동
              filterChain.doFilter(request, response);
              return;
          }
    		
          // 토큰에서 사용자 ID 불러옴
          String username = JwtUtils.getUsername(token, secretKey);
    
          // DB에서 사용자가 존재하는지 조회
          Optional<Member> result = memberRepository.findByUsername(username);
          Member member = null;
          // 존재한다면 사용자의 정보를 불러옴
          if(result.isPresent()) {
              member = result.get();
          } else {
              return;
          }
          // 사용자의 정보를 통해 검증 메서드 실시
          String memerUsername = member.getUsername();
          if(!JwtUtils.validate(token, memerUsername, secretKey)) {
              filterChain.doFilter(request, response);
              return;
          }
    
          // 사용자 인증 실시
          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                  member, null,
                  member.getAuthorities()
          );
           // 인증 정보를 SecurityContextHolder 에 저장
          SecurityContextHolder.getContext().setAuthentication(authentication);
          
          // 필터 체인의 다음 필터 진행
          filterChain.doFilter(request, response);
      }
    }

  • 다음은 SecurityConfig 클래스를 작성한 내용이다.

    @Configuration
    public class SecurityConfig {
    
      @Value("${jwt.secret-key}")
      private String secretKey;
    
      private final MemberRepository memberRepository;
      
      public SecurityConfig (MemberRepository memberRepository) {
          this.memberRepository = memberRepository;
      }
      
      @Bean
      public AuthenticationManager authenticationManager(
              AuthenticationConfiguration configuration) throws Exception {
          return configuration.getAuthenticationManager();
      }
    
      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity http) {
          try {
    
              http.csrf().disable()
                      .authorizeRequests()
                      .antMatchers("/jwt/*").permitAll() // /jwt로 시작하는 경로는 모든 사용자에게 허용
                      .antMatchers("/test/*").authenticated() // /test로 시작하는 경로는 인증 된 사용자만 허용
                      .anyRequest().authenticated()
                  .and()
                      // 새로만든 JWT 필터를 UsernamePasswordAuthentication 필터 앞에 추가
                      .addFilterBefore(new JwtFilter(secretKey, memberRepository), UsernamePasswordAuthenticationFilter.class)
                      // 스프링 시큐리티가 제공하는 기본 로그인 페이지 및 기능 차단
                      .formLogin().disable()
                      // 스프링 시큐리티의 세션 기능 off ( 토큰 기반은 세션이 필요 없어지기 때문에 )
                      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
              return http.build();
          } catch (Exception e) {
              throw new RuntimeException(e);
          }
      }
    }

  • 이렇게 작성하면 JWT 토큰에 대한 내용은 전부 작성된 것이고, 테스트를 위해
    Member 에 대한 클래스들( 컨트롤러, 서비스, 레포지토리 등 ) 을 생성하였다.
// ✅ Member 엔티티
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member implements UserDetails {

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

    @JsonProperty("email")
    @Column(name = "email")
    private String username;
    private String password;
    private String authority;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton((GrantedAuthority) () -> authority);
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

// ✅ MemberRepository 인터페이스
@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {
    public Optional<Member> findByUsername(String username);
}

// ✅ MemberLoginReq 클래스
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberLoginReq {

    @JsonProperty("email")
    private String username;
    private String password;
}

// ✅ MemberService 클래스
@Service
public class MemberService {
    @Value("${jwt.secret-key}")
    private String secretKey;

    @Value("${jwt.token.expired-time-ms}")
    private Integer expiredTimeMs;

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
    }

    // 💻 회원가입
    public void signup(MemberLoginReq memberLoginReq) {
        if(!memberRepository.findByUsername(memberLoginReq.getUsername()).isPresent())
            memberRepository.save(Member.builder()
                    .username(memberLoginReq.getUsername())
                    .password(passwordEncoder.encode(memberLoginReq.getPassword()))
                    .authority("ROLE_USER")
                    .build());
    }
    
	// 💻 로그인
    public String login(MemberLoginReq memberLoginReq) {
        Optional<Member> result = memberRepository.findByUsername(memberLoginReq.getUsername());
        Member member = result.get();
		
        // 입력받은 ID 와 패스워드를 DB와 비교하여 일치하면 토큰을 반환시켜줌
        if(result.isPresent() && passwordEncoder.matches(memberLoginReq.getPassword(), member.getPassword())) {
            return JwtUtils.generateAccessToken(member.getUsername(), secretKey, expiredTimeMs);
        } else {
            return null;
        }
    }
}

// ✅ MemberController 클래스
@RestController
@RequestMapping("/jwt")
public class MemberController {
    @Value("${jwt.secret-key}")
    private String secretKey;
    @Value("${jwt.token.expired-time-ms}")
    private Integer expiredTimeMs;
    private final MemberService memberService;
    private final AuthenticationManager authenticationManager;

    public MemberController(MemberService memberService, AuthenticationManager authenticationManager) {
        this.memberService = memberService;
        this.authenticationManager = authenticationManager;
    }
	
    // 💻 회원가입 기능
    @RequestMapping(method = RequestMethod.POST, value = "/signup")
    public ResponseEntity signup(@RequestBody MemberLoginReq memberLoginReq) {
        memberService.signup(memberLoginReq);

        return ResponseEntity.ok().body("ok");
    }

	// 💻 로그인 기능
    @RequestMapping(method = RequestMethod.POST, value = "/login")
    public ResponseEntity login(@RequestBody MemberLoginReq memberLoginReq) {
        return ResponseEntity.ok().body(memberService.login(memberLoginReq));
    }
}

  • Postman 으로 테스트를 진행해보겠다. 만료기한은 10000ms (10초) 로 설정

    1) 사용자 회원가입


    2) 사용자 로그인

    ➡ 로그인 하면 이렇게 JWT 토큰이 사용자에게 발급된다. 이것을 JWT 공식 홈페이지
      (https://jwt.io/) 에서 변환 시켜보면 아래와 같이 설정한 내용대로 토큰이 이루어져
      있는 것을 볼 수 있다.


    3) 발급 받은 토큰을 Authorization 에 넣어서 /test/test 란 경로로 접속을 시도해보면,
      토큰이 유효하면 접속이 될 것이고, 토큰이 유효하지 않으면 접속이 되지 않을 것이다.
      포스트맨에는 편리한 기능이 있어서 Authorization 을 선택한 뒤 Bearer Token 선택
      후 토큰을 입력하면 토큰을 헤더에 담아서 요청을 보낸다.

    Bearer 은 무엇인가❓
    Bearer 토큰은 OAuth 2.0 프로토콜에서 사용되는 토큰이다. 인증된 사용자를 대신하여 API에 접근할 수 있는 권한을 부여하며, JWT 토큰과 달리, 토큰 내에 사용자 정보를 포함시키지 않는다.

    즉, Bearer 토큰은 OAuth 2.0 프로토콜에서 사용되는 토큰으로, 인증된 사용자를 대신하여 API에 접근할 수 있는 권한을 부여합니다. Bearer 토큰은 JWT 토큰과 달리 사용자 정보를 포함시키지 않기 때문에, API에 접근할 때마다 사용자 정보를 함께 전달해야 한다. 이는 JWT 토큰보다는 불편하지만, 보안 측면에서는 더욱 안전하다.

    반면에, JWT 토큰은 JSON 형식으로 인코딩된 토큰으로, 사용자 정보와 권한 정보 등을 포함시켜 전달한다. 이를 통해 서버와 클라이언트 간의 인증 및 권한 부여에 사용되는데 JWT 토큰은 사용자 정보를 포함시키기 때문에, 클라이언트가 서버에 요청을 보낼 때마다 사용자 정보를 함께 전달할 필요가 없어서 효율적이다. JWT 토큰은 Bearer 토큰 중 하나이다.

    ➡ 요청을 보내보면 별도의 출력하는것은 만들지 않아서 빈 화면으로 보일 수 있지만,
      "200 OK" 라고 상태 코드가 뜬 것을 볼 수 있따.

    ➡ 하지만 10초가 지나고 다시 접속해보면 아래처럼 "403 Forbidden" 이 뜬다.

    ➡ 인텔리제이에서 오류 코드를 확인해보면, 토큰이 만료되었다고 출력되었다.


오늘의 느낀점 👀

  • 오늘은 토큰 기반 인증 시스템을 구현하는 것을 해봤는데, 확실히 스프링 시큐리티에서 자동으로 제공해주는 세션 기반 인증과는 별개로 어려웠다. 기본적으로 내가 필터를 작성하는 것부터 검증하는 것까지 토큰의 동작 원리를 모르면 작성을 하기가 힘들 것이다.

  • 특히 스프링 시큐리의 필터에 대해서 다시한번 생각해 보게되는 시간이었다. 이 필터들을 내가 직접 만들 수 있다는 것인데, 이 필터가 동작하는 원리를 직접 디버깅을 통해 눈으로 확인해 봤는데 그림으로만 보던 필터를 직접 요청이 어디로 전송되는지 보니깐 눈에 조금씩 보이기 시작했다.

  • 아직 완벽하게 JWT를 구현한 것은 아니지만, 다음주에 추가적으로 학습 하고나서 최종적으로 작성을 다시 해볼 계획이다.





[자료참고] https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC

profile
Backend Developer

0개의 댓글