웹/Back End

로그인 기능 구현 - JWT, Spring Security

물꼮이 2023. 11. 29. 06:48

Spring Security, JWT, Redis를 함께 사용하여 로그인 기능을 구현한 시스템 아키텍처의 도형도

1. 클라이언트는 아이디와 비밀번호를 입력하여 로그인을 시도합니다.
2. 서버는 입력된 정보를 확인하고, 인증이 성공한 경우 JWT 토큰을 생성합니다.
3. 서버는 생성된 JWT 토큰을 Redis에 저장하고, 클라이언트에게 응답으로 전달합니다.
4. 클라이언트는 응답으로 받은 JWT 토큰을 쿠키에 저장합니다.
5. 이후 클라이언트의 요청에는 쿠키에 저장된 JWT 토큰이 함께 전송됩니다.
6. 서버는 클라이언트의 요청에 포함된 JWT 토큰을 검증하고, Redis에서 해당 토큰을 조회하여 인증을 처리합니다.
7. 인증이 성공한 경우, 서버는 요청에 대한 응답을 반환합니다.
8. 로그아웃 시, 클라이언트는 쿠키에서 저장된 JWT 토큰을 삭제하여 인증 상태를 해제합니다.

 

Spring Boot 환경에서 JWT를 사용한 로그인 구현

JWT( JSON Web Token)는 웹 표준으로, 클라이언트와 서버 간의 정보를 안전하게 전송하기 위해 사용됩니다. 

  1. 의존성 추가
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    
    먼저, Spring Boot 프로젝트의 pom.xml 파일에 다음 의존성을 추가해야 합니다.
  2. JWT 토큰 생성 및 검증
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import java.util.Date;
    
    public class JwtUtils {
    
        private static final String SECRET_KEY = "your-secret-key";
        private static final long EXPIRATION_TIME = 86400000; // 24 hours
    
        public static String generateToken(String username) {
            Date now = new Date();
            Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);
    
            return Jwts.builder()
                    .setSubject(username)
                    .setIssuedAt(now)
                    .setExpiration(expirationDate)
                    .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                    .compact();
        }
    
        public static boolean validateToken(String token) {
            try {
                Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
    
        public static String getUsernameFromToken(String token) {
            Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
            return claims.getSubject();
        }
    }
    
    
    로그인 시 JWT 토큰을 생성하여 클라이언트에게 전달하고, 이후 요청에 대해 토큰의 유효성을 검증해야 합니다. 다음은 JWT 토큰을 생성하고 검증하는 예제 코드입니다.
  3. 로그인 API 구현
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class LoginController {
    
        @PostMapping("/login")
        public String login(@RequestBody LoginRequest request) {
            // 사용자 아이디와 비밀번호 검증 로직 작성
    
            // 유효한 경우 JWT 토큰 생성
            String token = JwtUtils.generateToken(request.getUsername());
    
            return token;
        }
    }
    
    
    로그인 API에서는 사용자가 입력한 아이디와 비밀번호를 검증한 후, 유효한 경우 JWT 토큰을 생성하여 클라이언트에게 반환해야 합니다. 다음은 Spring Boot에서 로그인 API를 구현하는 예제 코드입니다.
    1. 인증 필터 구현
      1. 인증 필터 구현
      JWT 토큰을 검증하기 위해 인증 필터를 구현해야 합니다. 인증 필터는 모든 요청에 대해 토큰의 유효성을 검증하고, 유효한 경우 요청을 허용합니다. 다음은 Spring Boot에서 인증 필터를 구현하는 예제 코드입니다.위의 코드에서 JwtTokenProvider는 JWT 토큰을 생성하고 검증하는 기능을 제공하는 클래스입니다. 이 클래스는 별도로 구현되어 있어야 합니다. JwtAuthenticationFilter는 OncePerRequestFilter를 상속받아 필터를 구현하고, doFilterInternal 메소드를 오버라이딩하여 요청에 대한 필터링 로직을 작성합니다.
    2. import org.springframework.security.core.Authentication;
      import org.springframework.security.core.context.SecurityContextHolder;
      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;
      
      public class JwtAuthenticationFilter extends OncePerRequestFilter {
      
          private JwtTokenProvider jwtTokenProvider;
      
          public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
              this.jwtTokenProvider = jwtTokenProvider;
          }
      
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
              String token = jwtTokenProvider.resolveToken(request);
      
              if (token != null && jwtTokenProvider.validateToken(token)) {
                  Authentication authentication = jwtTokenProvider.getAuthentication(token);
                  SecurityContextHolder.getContext().setAuthentication(authentication);
              }
      
              filterChain.doFilter(request, response);
          }
      }​

      1. 보안 설정
      마지막으로 Spring Boot 애플리케이션의 보안 설정을 추가해야 합니다. 이를 위해 WebSecurityConfigurerAdapter를 상속받은 설정 클래스를 작성합니다.위의 코드에서 /login 경로는 로그인 API를 허용하도록 설정되어 있습니다. 이외의 모든 요청은 인증이 필요하며, JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 이전에 등록하여 필터링이 이루어지도록 합니다.
    3. import org.springframework.beans.factory.annotation.Autowired;
      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.annotation.web.configuration.WebSecurityConfigurerAdapter;
      
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
          private JwtTokenProvider jwtTokenProvider;
      
          @Autowired
          public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
              this.jwtTokenProvider = jwtTokenProvider;
          }
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http
                  .csrf().disable()
                  .authorizeRequests()
                      .antMatchers("/login").permitAll()
                      .anyRequest().authenticated()
                      .and()
                  .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
          }
      }