컬쥐네 다락방

Spring Security 심화 공부 - JWT 회원가입, 로그인 기능 본문

웹 개발

Spring Security 심화 공부 - JWT 회원가입, 로그인 기능

코딩하는 갱얼쥐 2021. 4. 19. 01:24
Spring Security

최근 Spring Security를 사용해 회원 가입과 로그인을 구현하는 것을 공부했었는데, 이 과정에서 JWT를 이용해 사용자를 인증하는 방법에 대해 공부했다. 그땐 예제를 가지고 로그인과 회원가입 기능을 구현하는 것에 집중했었고, 이번에는 응용을 위해 좀 더 깊게 파고들어봤다.

 

스프링 시큐리티에서 어플리케이션 보안을 구성하는 영역은 크게 두 가지가 있다. 

https://dev.to/caffiendkitten/authentication-vs-authorization-25lc

 

인증(Authentication)은 보호된 리소스에 접근하는 대상, 즉 사용자에게 적절한 접근 권한이 있는지 확인하는 일련의 과정을 의미한다. 이 때 보호된 리소스에 접근하는 대상(사용자)을 접근 주체(Principal)이라고 하며 권한(Authorization)은 인증절차가 끝난 접근 주체가 보호된 리소스에 접근 가능한지를 결정하는 것을 의미한다. 이 때 권한을 부여하는 작업을 인가(Authorize)라고 한다.

 

인증은 아이디와 비밀번호를 입력 받아 로그인 하는 과정 자체를 의미하고, 권한이 필요한 리소스에 접근하려면 이러한 인증 과정을 거쳐야 한다. 스프링 시큐리티는 이런 매커니즘을 간단하게 만들 수 있도록 우리에게 여러 옵션을 제공한다.

 

Spring Security Filter

Spring Security 에서 사용하는 다양한 필터 기능들. 

 

 

  • SecurityContextPersistentFilter : SecurityContextRepository에서 SecurityContext를 가져와서 SecurityContextHolder에 주입하거나 반대로 저장하는 역할을 합니다.
  • LogoutFilter : logout 요청을 감시하며, 요청시 인증 주체(Principal)를 로그아웃 시킵니다.
  • UsernamePasswordAuthenticationFilter : login 요청을 감시하며, 인증 과정을 진행합니다.
  • DefaultLoginPageGenerationFilter : 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지로 넘어가게 합니다.
  • BasicAuthenticationFilter : HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다.
  • RememberMeAuthenticationFilter : SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체 요청이 있을 경우, RememberMe를 인증 토큰으로 컨텍스트에 주입합니다.
  • AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자로 취급합니다.
  • SessionManagementFilter : 요청이 시작된 이후 인증된 사용자인지 확인하고, 인증된 사용자일 경우 SessionAuthenticationStrategy를 호출하여 세션 고정 보호 매커니즘을 활성화 하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다.
  • ExceptionTranslationFilter : 필터체인 내에서 발생되는 모든 예외를 처리합니다.
  • FilterSecurityInterceptor : AccessDecisionManager로 권한부여처리를 위임하고 HTTP 리소스의 보안 처리를 수행합니다.
JWT(Json Web Token)

JWT는 json 객체를 통해 안전하게 정보를 전송하는 웹표준으로 '.'을 구분자로 세 부분으로 된 문자열로 이루어져 있습니다. 헤더는 토큰 타입과 알고리즘을 저장, 내용은 실제 전달할 정보, 서명은 위변조를 방지하기 위한 값이 들어갑니다.

 

JWT는 JSON 객체를 암호화 하여 만든 문자열 값으로 위, 변조가 어려운 정보라고 할 수 있습니다. 또, 다른 토큰들과 달리 토큰 자체에 데이터를 가지고 있다는 특징이 있습니다. JWT의 이러한 특징 때문에 사용자의 인증 요청시 필요한 정보를 전달하는 객체로 사용할 수 있습니다.

 

API 서버는 로그인 요청이 완료되면 클라이언트에게 회원을 구분할 수 있는 정보를 담은 JWT를 생성하여 전달합니다. 그러면 클라이언트는 이 JWT를 헤더에 담아서 요청을 하게 됩니다. 권한이 필요한 요청이 있을 때 마다 API 서버는 헤더에 담긴 JWT 값을 확인하고 권한이 있는 사용자인지 확인하고 리소스를 제공하게 됩니다.

 

이렇게 기존의 세션-쿠키 기반의 로그인이 아니라 JWT같은 토큰 기반의 로그인을 하게 되면 세션이 유지되지 않는 다중 서버 환경에서도 로그인을 유지할 수 있게 되고 한 번의 로그인으로 유저정보를 공유하는 여러 도메인에서 사용할 수 있다는 장점이 있습니다.

 

이 때 회원을 구분할 수 있는 정보가 담기는 곳이 바로 JWT의 payload 부분이고 이곳에 담기는 정보의 한 '조각'을 Claim 이라고 합니다. Claim은 name / value 한 쌍으로 이루어져 있으며 당연히 여러개의 Claim들을 넣을 수 있습니다.

(현재 진행중인 항해 하우스 프로젝트에서는 jwt의 payload에서 email이나 닉네임을 읽어와 사용자에 대한 정보를 찾거나 대화명으로 설정해주는 작업을 진행했어요 ! )

 

구현
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    private String secretKey = "webfirewood";

    // 토큰 유효시간 30분
    private long tokenValidTime = 30 * 60 * 1000L;
    
    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
        claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과 
                                                                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

토큰의 유효시간을 설정하고 사용자의 정보를 jwt 토큰에 담아 토큰을 생성해줍니다. 그 후 필요할때 토큰을 이용해서 유효성을 검사하거나 회원 정보를 추출하기 위한 컴포넌트를 만들어 줍니다.

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인합니다.
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

해당 컴포넌트를 이용해서 토큰을 이용해 유효성을 검증하고 JWT로부터 유저 정보를 받아 UsernamePasswordAuthenticationFilter로 전달해줍니다.

그리고 작성한 필터를 SecurityConfig 파일에 등록해주는 과정이 필요! 

@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    // authenticationManager를 Bean 등록합니다.
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class);
                // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
    }
}

이후에는 User 엔티티와 Repository등을 만들어 유저를 관리해주도록 만들어주면 끝 ! 

 

확실히 기본적인 스프링 시큐리티의 기능을 사용하다 jwt 기능을 연결해 사용하니 보안 문제에서도 더욱 안전성이 느껴지고 클라이언트와 통신할 때 헤더에 토큰만 담아서 보내주면 현재 이용하고 있는 사용자를 판단하기가 쉽다보니 더 많은 기능을 간편하게 구현할 수 있다고 생각한다. 

생각보다 Spring의 기능을 응용하니 구현 난이도도 쉽다고 생각되고 ! 

 

참고 사이트

아래는 JWT 기능 공부를 위해 참고했던 사이트 목록.

https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/

 

SpringBoot2로 Rest api 만들기(8) – SpringSecurity 를 이용한 인증 및 권한부여 - 아빠프로그래머의 좌충

이번 시간에는 SpringSecurity를 이용하여 api 서버의 사용 권한을 제한하는 방법에 대해 알아보도록 하겠습니다. 지금까지 개발한 api는 권한 부여 기능이 없어 누구나 회원 정보를 조회, 생성 및 수

daddyprogrammer.org

https://coding-start.tistory.com/153

 

Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!

오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적

coding-start.tistory.com

https://sjh836.tistory.com/165

 

spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal,

참조문서 https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-started http://springsource.tistory.com/80 https://okky.kr/article/382738 1. 스프링 시큐리티란?..

sjh836.tistory.com

 

 

 

https://okky.kr/article/382738

 

OKKY | 초보가 이해하는 스프링 시큐리티

저의 스프링 시큐리티 관련 예제는  깃허브 에서 제공합니다. (주석이 포함된 프로젝트는 주석이 너무 지저분하여 제외...) 1. 스프링 시큐리티란 무엇인가? 스프링 시큐리티를 이해하기 위해서

okky.kr

https://siyoon210.tistory.com/32

 

Spring Security - Filter, FilterChain

Spring Security (스프링 시큐리티) 스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있습니다. 일반적인 웹 환경에서 브라우저가

siyoon210.tistory.com

https://velopert.com/2389

 

[JWT] JSON Web Token 소개 및 구조 | VELOPERT.LOG

지난 포스트에서는 토큰 기반 인증 시스템의 기본적인 개념에 대하여 알아보았습니다. 이 포스트를 읽기 전에, 토큰 기반 인증 시스템에 대해서 잘 모르시는 분들은 지난 포스트를 꼭 읽어주세

velopert.com

webfirewood.tistory.com/115

 

SPRING SECURITY + JWT 회원가입, 로그인 기능 구현

이전에 서블릿 보안과 관련된 포스트(링크)를 작성했던 적이 있습니다. 서블릿 기반의 웹 애플리케이션에서 인증과 인가 과정을 간단하게 설명했습니다. 스프링에서는 마찬가지로 이런 인증과

webfirewood.tistory.com

 

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

2021.04.07~08 Spring 회원가입,로그인 기능 공부  (0) 2021.04.08
Comments