CSRF - API 서버

@RestController 대의 적인 상태를 전송함. 컨트롤러를 통해서 애플리케이션의 성격?을 알 수 있음

뷰를 랜더링 하지 않고 데이터만 전송을 함

 

@PostMapping(value = "")

데이터를 주로 보냄


@GetMapping(value = "/{num}", produces = MediaType.*APPLICATION_JSON_VALUE*)

전송하는 내용이 적음, ? 쿼리를 통해서 데이터를 전송함


@DeleteMapping(value = "/{num}", produces = MediaType.*TEXT_PLAIN_VALUE*)

삭제


@PutMapping(value = "/{num}", produces = MediaType.*TEXT_PLAIN_VALUE*)

업데이트

 

CSRF(Cross-Site Request Forgery)는 웹 보안 공격의 일종으로, 공격자가 피해자의 권한을 이용해 특정 웹 애플리케이션에서 원치 않는 작업을 수행하도록 하는 공격입니다. 이 공격은 사용자가 자신이 신뢰하는 사이트에 인증된 상태에서 다른 웹사이트나 링크를 클릭했을 때 발생할 수 있습니다. CSRF 공격은 사용자 몰래 이루어지기 때문에 매우 위험할 수 있습니다.

CSRF 공격의 작동 원리

  1. 사용자 인증: 사용자가 웹 애플리케이션에 로그인하면, 서버는 사용자에게 세션 쿠키를 발급합니다. 이 쿠키는 사용자가 서버에 인증된 사용자임을 나타내며, 이후의 요청에서 사용됩니다.
  2. 악의적인 요청: 공격자는 피해자가 이미 로그인한 상태임을 이용해 악의적인 요청을 만듭니다. 이 요청은 피해자가 의도하지 않은 행동(예: 비밀번호 변경, 돈 송금)을 수행하게 합니다.
  3. 자동 실행: 피해자가 공격자의 사이트를 방문하거나, 공격자가 만든 악의적인 링크를 클릭하게 되면, 피해자는 모르는 사이에 이 요청을 실행하게 됩니다. 브라우저는 사용자의 세션 쿠키를 자동으로 포함하기 때문에, 서버는 이를 정상적인 요청으로 받아들입니다.

CSRF 방어 방법

  1. CSRF 토큰 사용: 서버는 폼이나 요청에 고유한 토큰을 포함시킵니다. 이 토큰은 사용자가 요청을 보낼 때 함께 전송되어야 하며, 서버는 이 토큰을 검증하여 요청의 유효성을 확인합니다. 공격자는 이 토큰을 알 수 없으므로, CSRF 공격이 어려워집니다.
  2. Referer 헤더 검사: 요청의 Referer 헤더를 확인하여 요청이 신뢰할 수 있는 출처에서 발생했는지 확인하는 방법입니다. 하지만 이 방법은 완벽하지 않으며, Referer 헤더가 항상 존재하지 않을 수 있습니다.
  3. SameSite 쿠키 속성: 쿠키에 SameSite 속성을 설정하여, 동일한 사이트에서만 쿠키가 전송되도록 설정할 수 있습니다. 이는 CSRF 공격의 위험을 줄여줍니다.

CSRF는 사용자가 모르는 사이에 중요한 작업이 실행될 수 있기 때문에 매우 위험한 공격입니다. 이를 방어하기 위해서는 적절한 보안 조치가 필요합니다.

API 서버는 애플리케이션 프로그래밍 인터페이스(API)를 제공하는 서버로, 클라이언트 애플리케이션이 서버의 기능이나 데이터를 사용할 수 있도록 중계 역할을 합니다. API 서버는 주로 REST(Representational State Transfer)나 GraphQL 등의 웹 API 형식으로 클라이언트와 통신하며, 다양한 애플리케이션 간에 데이터를 교환하고 기능을 수행할 수 있게 합니다.

API 서버의 주요 역할

  1. 요청 처리: 클라이언트가 보낸 HTTP 요청을 받아서, 해당 요청에 맞는 데이터를 반환하거나 특정 작업을 수행합니다.
  2. 데이터 관리: API 서버는 종종 데이터베이스와 연동되어 클라이언트의 요청에 따라 데이터를 읽고, 쓰고, 업데이트하며, 삭제합니다.
  3. 인증 및 권한 부여: API 서버는 클라이언트의 요청을 처리하기 전에 인증(Authentication)과 권한 부여(Authorization)를 통해 요청이 올바른 사용자인지, 그리고 해당 요청을 수행할 권한이 있는지를 확인합니다.
  4. 비즈니스 로직 수행: API 서버는 클라이언트 요청에 대해 단순히 데이터를 반환하는 것 외에도, 특정 비즈니스 로직을 수행합니다. 예를 들어, 주문 처리, 결제, 데이터 분석 등의 작업이 API 서버에서 이루어질 수 있습니다.

API 서버의 특징

  1. REST API: 대부분의 API 서버는 RESTful API를 따릅니다. REST는 리소스를 중심으로 설계된 아키텍처 스타일로, 클라이언트와 서버 간의 요청과 응답을 쉽게 이해하고 처리할 수 있게 해줍니다. REST API는 주로 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 서버의 리소스에 접근합니다.
  2. GraphQL API: REST와는 다른 접근 방식을 취하는 GraphQL은 클라이언트가 필요로 하는 정확한 데이터만을 요청할 수 있게 해주는 쿼리 언어입니다. 클라이언트는 자신이 필요한 데이터를 정의하여 서버에 요청하며, 서버는 그에 맞는 응답을 제공합니다.
  3. JSON 및 XML 형식: API 서버는 데이터를 주고받을 때 주로 JSON(JavaScript Object Notation) 형식을 사용합니다. JSON은 가볍고, 사람과 기계가 읽기 쉬운 형식입니다. XML(Extensible Markup Language)도 사용되지만, 현재는 주로 JSON이 선호됩니다.
  4. 스케일링 및 확장성: API 서버는 많은 클라이언트 요청을 처리할 수 있도록 설계됩니다. 이를 위해 로드 밸런싱, 캐싱, 서버 클러스터링 등의 기술이 사용됩니다.

API 서버의 구성 요소

  1. 라우팅(Routing): API 서버는 클라이언트의 요청 URL을 해석하여 어떤 동작을 수행할지 결정하는 라우팅 기능을 가집니다.
  2. 컨트롤러(Controller): 라우팅을 통해 전달된 요청을 처리하는 로직을 포함합니다. 예를 들어, 클라이언트가 특정 사용자 정보를 요청하면, 해당 요청을 처리하는 컨트롤러가 데이터베이스에서 정보를 조회하고, 이를 클라이언트에 반환합니다.
  3. 모델(Model): API 서버에서 사용되는 데이터 구조를 정의하며, 데이터베이스와의 상호작용을 처리합니다.
  4. 미들웨어(Middleware): 요청 처리 과정 중간에 위치하여, 요청과 응답을 가로채어 특정 작업을 수행하는 역할을 합니다. 예를 들어, 인증, 로깅, 데이터 변환 등이 미들웨어에서 처리됩니다.

API 서버는 웹 애플리케이션의 백엔드를 구성하는 중요한 요소로, 다양한 클라이언트(웹, 모바일 애플리케이션 등)와 통신하며 기능을 제공합니다.


  @Bean
  public ApiCheckFilter apiCheckFilter() {
    return new ApiCheckFilter(new String[]{"/notes/**"});
  }
public class ApiCheckFilter extends OncePerRequestFilter {
  //주소의 패턴
  private String[] pattern;
  //요청되는 주소와 패텅의 주소를 비교해주는 객체
  private AntPathMatcher antPathMatcher;

    //ApiCheckFilter 를 초기화 하는 생성자 클래스
  public ApiCheckFilter(String[] pattern) {
    this.pattern = pattern;
    antPathMatcher = new AntPathMatcher();
  }
 }

OncePerRequestFilter 에 있는 doFilter 를 재정의 해줌

ApiCheckFilter의 기능 2가지

  • 요청된 주소와 허락된 패턴(허락된 rest 주소)와 일치하는지 비교
  • 일치 된 주소에 토큰의 유무를 확인
 @Override
  protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain)
      throws ServletException, IOException {
    log.info("request.getRequestURI()" + request.getRequestURI());
    log.info("request.getContextPath()" + request.getContextPath());
    log.info("REQUEST match: " + request.getContextPath(), request.getRequestURI());

    boolean check = false;
    for (int i = 0; i < pattern.length; i++) {
      log.info(">> "+ antPathMatcher.match(request.getContextPath()
          + pattern[i], request.getRequestURI()));
      //요청된 주소와 패턴 주소가 일치 할 경우에 조정한다
      if (antPathMatcher.match(request.getContextPath()
          + pattern[i], request.getRequestURI())) {
        check = true;
        break;
      }
      // 요청주소와 패턴이 일치할 경우 분기
      if(check) {
        log.info("check ; " + check);
        //토큰 유무에 대하여 checkAuthHeader를 통해 확인
        boolean checkTokenHeader = checkAuthHeader(request);
        //checkAuthHeader 는 token 유무에 의한 분기
        if(checkTokenHeader){
          filterChain.doFilter(request, response);
          return;
        }
        else{
          response.setStatus(HttpServletResponse.SC_FORBIDDEN);
          response.setContentType("application/json;charsert=uft-8");
          JSONObject jsonObject = new JSONObject();
          String message = "FAIL CHECK API TOKEN";
          jsonObject.put("code","403");
          jsonObject.put("message",message);
          PrintWriter printWriter = response.getWriter();
          printWriter.println(jsonObject);
          return;
        }
      }
      filterChain.doFilter(request,response);// 요청주소와 패턴이 불일치
    }
  }

  private boolean checkAuthHeader(HttpServletRequest request) {
    boolean checkResult = false;
    return checkResult;
  }

요청된 주소가 해당하는 주소와 같은 경우, 실행을 시켜줌

 httpSecurity.authorizeHttpRequests(
        auth -> auth
            .requestMatchers(new AntPathRequestMatcher("/notes/**")).permitAll()
            .requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll()
            .requestMatchers(new AntPathRequestMatcher("/error/**")).permitAll()
            .anyRequest().denyAll());

.requestMatchers(new AntPathRequestMatcher("/notes/**")).permitAll().requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll().requestMatchers(new AntPathRequestMatcher("/error/**")).permitAll() 를 제외한 나머지 주소는 요청을 거부한다

 //addFilterBefore는 일반적인 필터링 순서보다 앞쪽에서 필터링 하도록 순서 조정.
    httpSecurity.addFilterBefore(
        apiCheckFilter(), UsernamePasswordAuthenticationFilter.class);
 //요청된 주소와 패턴 주소가 일치 할 경우에 조정한다
 if (antPathMatcher.match(request.getContextPath()
          + pattern[i], request.getRequestURI())) {
        check = true;
        break;
      }
     // 요청주소와 패턴이 일치할 경우 분기
      if(check) {
        log.info("check ; " + check);
        filterChain.doFilter(request, response);
        return;
      }else{
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charsert=uft-8");
        JSONObject jsonObject = new JSONObject();
        String message = "FAIL CHECK";
        jsonObject.put("code","403");
        jsonObject.put("message",message);
        PrintWriter printWriter = response.getWriter();
        printWriter.println(jsonObject);
        return;
      }
      filterChain.doFilter(request,response);
    }
    if(check){

    }
    filterChain.doFilter(request, response);

jjwt관련 dependency

    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.12.6</version>
            <scope>runtime</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.12.6</version>
            <scope>runtime</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-gson -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-gson</artifactId>
            <version>0.12.6</version>
        </dependency>

 


@Log4j2
public class JWTUtil {
  private String secretKey = "1234567890abcdefghijklmnopqrstuvwxyz";
  private  long expire = 60*20*30;
  public String generateToken(String content) throws Exception{
    return Jwts.builder()
        .issuedAt(new Date())
        .expiration(Date.from(ZonedDateTime.now().plusMinutes(expire).toInstant()))
        .claim("sub",content)
        .signWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
        .compact();
  }
  //JWT 검증 및 email 추출
  public String validateAndExtract(String tokenStr) throws  Exception{
    log.info("Jwts getClass: " + Jwts.parser()
        .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
        .build().parse(tokenStr));
    Claims claims =(Claims) Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))).build()
        .parse(tokenStr).getPayload();
    return (String) claims.get("sub");
  }
}
@SpringBootTest
class JWTUtilTests {
  private JWTUtil jwtUtil;
  @BeforeEach
  public void testBefore(){
    System.out.println("test before....");
    jwtUtil = new JWTUtil();
  }
  @Test
  public void testEncode() throws  Exception{
    String email ="user95@a.a";
    String token = jwtUtil.generateToken(email);
    System.out.println(token);
  }
}

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

Encoded에 위의 토큰을 넣어주고 VERIFY SIGNATURE 칸 안에 1234567890abcdefghijklmnopqrstuvwxyz secretKey를 넣어줌

 

 @Test
  public void testValidate() throws  Exception{
    String email ="user95@a.a";
    String token = jwtUtil.generateToken(email);
    Thread.sleep(3000);
    String resultEmail = jwtUtil.validateAndExtract(token);
    System.out.println(token);
  }

토큰을 생성 : 사용자가 로그인 할 때 생성해줌

jwt 필터를 생성 해주고 api 필터를 사용해서 이미 있는 토큰인지 확인?

public class ApiLoginFilter extends AbstractAuthenticationProcessingFilter {
  private JWTUtil jwtUtil;

  public ApiLoginFilter(String defaultFilterProcessUrl, JWTUtil jwtUtil){
    super(defaultFilterProcessUrl);
    this.jwtUtil = jwtUtil;
  }
}
@Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                          FilterChain chain, Authentication authResult)
      throws IOException, ServletException {
    logger.info("successfulAuthentication :: authResult" + authResult.getPrincipal());
    String email = ((ClubMemberAuthDTO)authResult.getPrincipal()).getEmail();
    String token = null;
    try{
      token = jwtUtil.generateToken(email);
      response.setContentType("text/plain"); //토큰을 발행
      response.getOutputStream().write(token.getBytes());
      logger.info("generated token ; " + token);
    }catch (Exception e) {e.printStackTrace();}
  }

 @Override
  public Authentication attemptAuthentication(HttpServletRequest request, 
  HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException {
    String email = request.getParameter("email");
    String pass = request.getParameter("pw");
    UsernamePasswordAuthenticationToken authToken =
        new UsernamePasswordAuthenticationToken(email, pass);
    return getAuthenticationManager().authenticate(authToken);
  }

csrf - ssr 방식

CorseFilter 를 사용하는 이유 , 특정 페이지를 지정해주는것

 

API

@RestController
@Log4j2
@RequestMapping("/notes/")
@RequiredArgsConstructor
public class NoteController {
  private final NoteService noteService;

  @PostMapping(value = "")
  public ResponseEntity<Long> register(@RequestBody NoteDTO noteDTO) {
    log.info("noteDTO: "+noteDTO);
    Long num = noteService.register(noteDTO);
    return new ResponseEntity<>(num, HttpStatus.OK);
  }

  @GetMapping(value = "/{num}", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<NoteDTO> read(@PathVariable("num") Long num) {
    log.info("read... num: " + num);
    return new ResponseEntity<>(noteService.get(num), HttpStatus.OK);
  }

  @DeleteMapping(value = "/{num}", produces = MediaType.TEXT_PLAIN_VALUE)
  public ResponseEntity<String> remove(@PathVariable("num") Long num) {
    log.info("delete... num: " + num);
    noteService.remove(num);
    return new ResponseEntity<>("removed", HttpStatus.OK);
  }

  @PutMapping(value = "/{num}", produces = MediaType.TEXT_PLAIN_VALUE)
  public ResponseEntity<String> modify(@RequestBody NoteDTO dto) {
    log.info("modify... dto: " + dto);
    noteService.modify(dto);
    return new ResponseEntity<>("modified", HttpStatus.OK);
  }
}