Signup CSRF Token Release and User Model Creation

민준·2023년 2월 23일
0
post-thumbnail

지난 글에 인증받은 사용자와 그렇지 않은 사용자의 요청에 대해 차이를 두는 시큐리티 세팅까지 구현하였고
이번에는 회원가입을 구현합니다.

1. 회원가입 기능 구현하기

localhost로 접속해 '가입하기' 버튼을 누르면 회원가입 창으로 변합니다.
현재는 회원가입 기능이 되지 않습니다.

signup.jsp

src/main/webapp/WEB/INF/views/auth 경로의 'signup.jsp' 파일을 엽니다.
그 안에서 회원가입 인풋을 찾아 아래와 같이 form class 옆에 '태그 속성'을 추가합니다.

<form class="login__input" action="/auth/signup" method="post">

그러면 아래 네 건의 데이터(가입정보)를 요청하면 데이터베이스에 인서트하는데 이를 위해 'POST' 요청을 합니다.

AuthController.java

이제 컨트롤러를 추가합니다. 'com.cos.photogramstart' 아래 'web' 패키지를 하나 추가합니다.

package com.cos.photogramstart.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller // 1. IoC 등록, 2. 파일을 리턴함.
public class AuthController {

	@GetMapping("/auth/signin")
	public String signinForm() {
		return "auth/signin";
	}
}

이렇게 컨트롤러를 추가하고 저장하면 - 오류가 납니다.
이유는 기존에 만들었던 'ViewControllerTest'에 이 도메인이 등록돼있기 때문입니다.
이 컨트롤러는 테스트용으로 사용한 것이라 이 클래스 위의 '@Constroller'를 주석처리 합니다.

그리고 다시 새로 만든 컨트롤러에 아래 코드를 추가합니다.
이때 코드를 복붙하면서 수정할 부분을 빼먹진 않았는지 확인해야합니다. 저는 하나 안고쳐서 오류가 났습니다.

@GetMapping("/auth/signup")
public String signupForm() {
	return "auth/signup";
}

POST Method

다시 아래의 코드를 추가합니다. 위에서는 'GET' 요청으로 회원가입 페이지를 응답했지만
아래에서는 'POST' 요청으로 회원가입이 완료되면 로그인 페이지로 응답하게 됩니다.

@PostMapping("/auth/signup")
public String signup() {
	return "auth/signin"; // 회원가입 성공하면 로그인 페이지로 이동.
}

이제 회원가입 페이지에서 정보를 입력하고 [가입] 버튼을 누르면 창이 넘어가긴 하는데 아직 403 오류창이 나타납니다.
[가입하기]를 누르면 '/auth/signup'으로 이동하고 여기서 '가입'을 누르면 '/auth/signin'으로 리턴됩니다. > 되어야합니다.
지금 그렇지 않은 이유는 'CSRF 토큰'이 활성화 되어있기 때문입니다.

CSRF 토큰 해제하기

클라이언트가 폼에 데이터를 넣고 서버로 전송하면
서버를 시큐리티가 감싸고 있다가 데이터를 CSRF 토큰 검사를 합니다.

사용자가 [가입하기]를 누르면 클라이언트는 서버에 '/auth/signup'을 요청하고
서버는 'signup.jsp' 파일로 응답합니다. 이때 시큐리티가 이 파일에 CSRF 토큰을 심어놓습니다.
그러면 사용자가 데이터를 넣을 폼(Input Tag)에 임의의 난수값이 생기게 됩니다.
그리고 사용자가 데이터를 넣어 [가입]을 눌러 요청하면 서버는 CSRF 토큰이 있는지 검사합니다.

아래 캡쳐화면처럼 다른 경로로 데이터를 보내오는지,
서버가 보낸 페이지에 정상적으로 데이터를 넣어 요청이 다시 오는지,
즉 정상적인 사용자인지 아닌지를 확인하는 것입니다.

여기서는 CSRF 토큰을 비활성화해 사용하지 않을겁니다.

SecurityConfig.java

지난 글에서 사용자 인증 접근 구별에 대해 작성한 코드에 아래 코드를 추가합니다. 👉 지난 글 참고

http.csrf().disable();

이제 서버는 사용자가 포스트맨을 사용하든 정상적인 페이지에서 접근하든 상관하지 않습니다.
이제 가입정보를 기입하고 [가입]을 누르면 로그인창으로 이동합니다.
다만 아직 입력한 데이터를 어딘가에 저장하지는 않습니다.

그런데, POST 요청 방식이라 그런걸까요 ?
두 번째 캡쳐화면을 보면 분명 로그인 창이 맞는데, 도메인은 '/auth/signup' 입니다.
개발자도구 네트워크에서도 signup 요청, 응답이 잘 되었다고 나타납니다.

Data Transfer Object (DTO)

web 패키지 이하 두 개의 패키지를 생성합니다. 경로는 'web/dto/auth'이 됩니다.
여기에 'SignupDto' 클래스를 추가합니다. 'Dto'는 요청, 응답 두 가지가 있는데 여기에서는 요청하는 것입니다.

DTO 는 통신을 할 때 필요한 데이터를 담아두는 오브젝트입니다.
여기서는 회원가입 시 입력하는 데이터를 담도록 합니다.

package com.cos.photogramstart.web.dto.auth;

import lombok.Data;

@Data // Getter, Setter 를 만들어주는 어노테이션
public class SignupDto {
	
	private String username;
	private String password;
	private String email;
	private String name;

}

이제 'AuthController'로 가서 컨트롤러 안쪽에 'Logger'를 추가하고 'signup' 메서드(POST)를 수정합니다.
'log.info()'는 문자열만 받을 수 있습니다.

@PostMapping("/auth/signup")
public String signup(SignupDto signupDto) {
	log.info(signupDto.toString());
	return "auth/signin";
} 

다시 회원가입을 시도합니다. 틀에 입력한 데이터가 콘솔창에 표시되었습니다!

이렇게 전송한 데이터를 서버에서 받을 수 있게 되었습니다. 이제는 DB에 INSERT 해야합니다.
이를 위해서는 모델이 필요합니다.

User Model

com.cos.photogramstart 안에 domain.user 패키지를 만들고 안에 User 클래스를 생성합니다.

package com.cos.photogramstart.domain.user;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// JPA ( Java Persistence API - 자바로 데이터를 영구적으로 저장(DB)할 수 있는 API 제공)
@AllArgsConstructor // 전체 생성자
@NoArgsConstructor  // 빈 생성자
@Data               // Getter, Setter 
@Entity             // DB에 테이블을 생성해줌
public class User {
	
	@Id // primary 키가 없어서 오류가 난다.. ? 난 왜 안나지.? 아무튼 프라이머리 키가 되어줍니다.
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라간다.
	private int id;
	
	private String username;
	private String password;
	private String name;
	private String website;
	private String email;
	private String phone;
	private String gender;
	
	private String profileImageUrl;
	private String role;
	
	private LocalDateTime createDate;
	
	@PrePersist // 데이터베이스에 값이 인서트되기 직전에 이 어노테이션을 걸어주면 실행되도록 합니다.
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}
}

이제 'mariadb'를 확인합니다. 여기서 DB에 위의 테이블들이 생겨나야 하지만

문제가 생겼습니다.

저는 테이블 생성이 되지 않습니다. application.yml 파일에서 경로나 포트번호 등등
이것저것 구글링해서 바꿔보고 서버 재시동, DB 껐다 켜보고 다 하고 있는데 되지 않습니다.


원인을 찾았습니다. 처음 이 프로젝트를 시작할 때 추가한 코드 한 줄이 원인이었습니다. 👉 지난 글
처음에 분명 앱이 실행되지 않아 서버 구동이 안되었고 DB 연동 문제가 있어서
블로그 참고하여 '@SpringBootApplication'에 아래 코드를 추가했고 문제가 해결되었는데,

(exclude={DataSourceAutoConfiguration.class})

이제와서 DB에 테이블을 만드는 등 연동시키려고 하니 이 코드가 문제가 되는 것이었습니다.
@SpringBootApplication 어노테이션은 스프링 부트의 가장 기본적인 설정을 선언하는데,
이 어노테이션을 command 키를 누른채로 클릭하면 아래와 같은 인터페이스 코드를 볼 수 있습니다.

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.repository.Repository;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;

}

이 어노테이션이 크게 3가지의 기능을 가지고 있다는데 솔직히 지금의 저는 이게 어디가지 관여하는지 잘 모르겠고요.
다만 이름만 봐서는 데이터 소스를 자동으로 설정해주는 기능을 제외하는 코드를 지웠더니
웹서버를 통해 데이터베이스에 테이블을 생성할 수 있게 되었습니다.

아마 분명히 저와 같은 경로로 따라가다가 저와 같이 막히는 분들이 계실까 싶어 일단 해결 방식을 공유합니다.

그리고 앱이 정상적으로 구동하면 아래와 같은 쿼리문이 콘솔창에 추가로 나타납니다.

Hibernate: drop table if exists User
Hibernate: create table User (id integer not null auto_increment, bio varchar(255), createDate datetime(6), email varchar(255), gender varchar(255), name varchar(255), password varchar(255), phone varchar(255), profileImageUrl varchar(255), role varchar(255), username varchar(255), website varchar(255), primary key (id)) engine=InnoDB

해 - 결

profile
백엔드 포지션 공부 중입니다.

0개의 댓글