반응형

오늘은 Spring Security를 활용하여 JWT(Json Web Token) 기반 인증 시스템을 구현하는 방법을 배워보겠습니다. Spring Security는 인증 및 권한 관리를 효율적으로 처리할 수 있는 강력한 프레임워크로, JWT와의 연동을 통해 더욱 확장 가능한 인증 시스템을 구축할 수 있습니다.


1. Spring Security의 역할

Spring Security는 다음과 같은 작업을 자동으로 처리합니다:

  • 인증(Authentication): 사용자의 신원을 확인
  • 인가(Authorization): 사용자의 권한을 확인
  • 보안 필터 체인(Security Filter Chain): 요청과 응답 사이의 보안 검증

JWT와 Spring Security를 연동하면 Stateless 인증을 효율적으로 구현할 수 있습니다.


2. 의존성 추가

pom.xml에 Spring Security 관련 의존성을 추가합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

3. Spring Security 설정 파일 생성

Spring Security의 설정 파일을 작성합니다.

SecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 http) throws Exception {
        http.csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/api/login", "/api/register").permitAll() // 인증 불필요한 경로
                .anyRequest().authenticated(); // 그 외 경로는 인증 필요
        return http.build();
    }
}

4. JWT 필터 구현

JWT를 검증하는 필터를 작성하여 Security Filter Chain에 추가합니다.

JwtAuthenticationFilter.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

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 static final String SECRET_KEY = "my-secret-key";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.replace("Bearer ", "");
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();

            if (claims != null) {
                String username = claims.getSubject();

                if (username != null) {
                    JwtUserDetails userDetails = new JwtUserDetails(username); // 사용자 정보
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

5. 필터 등록

JwtAuthenticationFilter를 Spring Security의 필터 체인에 등록합니다.

SecurityConfig.java 업데이트

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/login", "/api/register").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

6. JWT 기반 API 구현

로그인 API

@PostMapping("/api/login")
public String login(@RequestBody Map<String, String> credentials) {
    String username = credentials.get("username");
    String password = credentials.get("password");

    if ("user".equals(username) && "password".equals(password)) {
        return JwtUtil.generateToken(username);
    } else {
        throw new RuntimeException("Invalid credentials");
    }
}

보호된 API

@GetMapping("/api/secure/data")
public String secureData() {
    String username = SecurityContextHolder.getContext().getAuthentication().getName();
    return "Hello, " + username + "! You have accessed a secure endpoint.";
}

7. Postman 테스트

  1. 로그인 API 호출:
    • URL: http://localhost:8080/api/login
    • Method: POST
    • Body:
      {
          "username": "user",
          "password": "password"
      }
      
    • 응답: JWT 토큰 반환
  2. 보호된 API 호출:
    • URL: http://localhost:8080/api/secure/data
    • Method: GET
    • Header:
      • Key: Authorization
      • Value: Bearer <발급받은 JWT 토큰>
    • 응답: 인증된 사용자 이름 포함 메시지 반환

8. 추가 보안 고려사항

  • 토큰 재발급(Refresh Token): 만료된 토큰의 갱신을 위해 Refresh Token을 구현할 수 있습니다.
  • 토큰 만료 시 로그아웃 처리: 보안성을 높이기 위해 만료된 토큰의 처리를 철저히 관리해야 합니다.
  • HTTP/2 및 HTTPS 사용: 네트워크 상에서 데이터 암호화를 강화합니다.
반응형

+ Recent posts