반응형

오늘은 RESTful API에서 사용자 인증(Authentication)과 권한 부여(Authorization)를 구현하는 방법에 대해 학습합니다. API 보안을 강화하고, 역할(Role) 기반 접근 제어를 통해 민감한 데이터의 접근을 관리합니다.


1. 인증(Authentication)과 권한 관리의 이해

  1. 인증(Authentication)
    사용자의 신원을 확인하는 과정입니다. 주로 사용자 이름, 비밀번호 또는 토큰을 사용합니다.
    • 예: 로그인, OAuth2.0 인증 등
  2. 권한 관리(Authorization)
    인증된 사용자가 특정 리소스에 접근할 수 있는지를 확인합니다.
    • 예: 관리자만 특정 API를 호출 가능

2. Spring Security를 사용한 인증 구현

1) Spring Security 의존성 추가

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

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

2) Security 설정 클래스 작성

Spring Security의 기본 설정을 커스터마이징합니다.

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.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // CSRF 비활성화
            .authorizeRequests()
            .antMatchers("/api/public/**").permitAll() // 공개된 API
            .antMatchers("/api/admin/**").hasRole("ADMIN") // 관리자 전용
            .anyRequest().authenticated() // 나머지 요청 인증 필요
            .and()
            .httpBasic(); // HTTP Basic 인증
    }
}

3. JWT를 활용한 인증

1) JWT란?

JWT(Json Web Token)는 사용자 인증 및 정보 교환에 사용되는 토큰 기반 인증 방식입니다.
구성: Header.Payload.Signature

2) JWT 토큰 생성 로직

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
public class JwtService {

    private static final String SECRET_KEY = "mySecretKey";
    private static final long EXPIRATION_TIME = 86400000; // 1일

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
}

3) JWT 인증 필터

사용자의 요청에 포함된 토큰을 검증하는 필터를 생성합니다.

import io.jsonwebtoken.Jwts;
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 static final String SECRET_KEY = "mySecretKey";

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

        if (token != null && token.startsWith("Bearer ")) {
            String jwt = token.substring(7);
            try {
                String username = Jwts.parser()
                        .setSigningKey(SECRET_KEY)
                        .parseClaimsJws(jwt)
                        .getBody()
                        .getSubject();

                if (username != null) {
                    Authentication auth = new CustomAuthentication(username);
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e) {
                SecurityContextHolder.clearContext();
            }
        }

        filterChain.doFilter(request, response);
    }
}

4. 권한(Role) 기반 접근 제어

1) 역할(Role) 설정

사용자마다 역할을 설정하여 리소스 접근을 제어합니다.

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.List;

public class User {

    private String username;
    private String password;
    private List<GrantedAuthority> roles;

    public User(String username, String password, List<String> roles) {
        this.username = username;
        this.password = password;
        this.roles = roles.stream()
                          .map(SimpleGrantedAuthority::new)
                          .toList();
    }

    // Getter & Setter 생략
}

2) 관리자 전용 API

@PreAuthorize 어노테이션을 사용하여 특정 역할만 접근 가능하게 설정합니다.

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/dashboard")
    public String adminDashboard() {
        return "관리자 대시보드";
    }
}

5. 테스트

  1. 로그인 API 호출
    사용자가 로그인을 통해 JWT 토큰을 발급받습니다.
  2. POST /api/auth/login Headers: Content-Type: application/json Body: { "username": "admin", "password": "admin123" }
  3. JWT를 포함하여 요청 보내기
    발급받은 JWT 토큰을 사용하여 인증된 요청을 보냅니다.
  4. GET /api/admin/dashboard Headers: Authorization: Bearer <JWT 토큰>
  5. 권한 없는 사용자의 요청
    관리자 전용 API에 접근하려는 일반 사용자는 403 Forbidden 응답을 받습니다.

6. 보안 최적화

  1. HTTPS 적용: 모든 데이터를 암호화하여 전송합니다.
  2. 토큰 만료 시간 설정: 짧은 만료 시간으로 보안 강화
  3. Refresh Token 사용: 만료된 액세스 토큰을 재발급합니다.
반응형

+ Recent posts