스프링부트 - 회원가입

ezi·2023년 11월 6일
0

개념 ) CSRF 토큰

우리 서버는 Security가 감싸고 있음
따라서 클라이언트가 전송한 것이 서버에 도착 전에 Security가 먼저 검사를 함

무슨 검사 ? CSRF 토큰 검사
CSRF 토근 검사? 사용자가 회원가입 페이지 를 요청하면
서버에서 signup.jsp 를 응답해주겠지?
근데 여기서 서버가 signup.jsp 파일에 CSRF 토큰을 심어서 돌려준다.

자, 여기서 회원가입 페이지엔 input 태그가 두개(데이터1, 데이터2)가 있다고 하자
그럼 서버가 응답한 signup.jsp 의 input 태그엔 임시 난수값이 생겨서 돌아온다.

나의 signup.jsp 파일엔 안보이지만, 아래와 같이csr="csr" 가 추가되었다고 생각하면 된다.

<input type="text" name="username" placeholder="유저네임" required="required" csr="csr"/>

input태그에 데이터를 담아서 데이터1과 데이터2를 서버에게 전송하여 회원가입 요청을 했다고 하자

그럼 서버가 내가 만들어준 CSRF 토큰이 있는지 확인한다.
만약 맞다면 "아, 내가 보내준 signup.jsp 파일이 맞고 이 페이지에서 정상적으로 데이터를 보냈구나" 하는거다.

정상적이지 않은 요청이 뭐냐? 싶을 수 있는데,
간단한 예를 들자면 포스트맨에서 post를 보내는 경우라고 생각하면 됩니다.

CSRF 토큰 해제하고 싶은데 ?

http.csrf().disable();

package com.cos.photogramstart.config;

import org.springframework.beans.factory.annotation.Configurable;
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.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity // 해당 파일로 시큐리티 활성화시킴
@Configurable // IoC 등록
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http); -> super 삭제 : 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/","/user/**","/image/**","/subscribe/**","/comment/**").authenticated() // 인증이 필요한 페이지들
                .anyRequest().permitAll() // 이외의 페이지는 접근 허용
                .and()
                .formLogin() //인증이 필요한 페이지들은 접근하면 로그인하도록
                .loginPage("/auth/signin")// 로그인 페이지는 "~"
                .loginProcessingUrl("/"); // 로그인이 성공적으로 됐다면 "/"으로 ㄱㄱ
    }
}



1. form 데이터 db에 저장하기

목표 : jsp form 에 있는 username, password, email, name 을 회원가입 후 유저로 db에 저장

//jsp 
<form class="login__input" action="/auth/signup" method="post">
	<input type="text" name="username" placeholder="유저네임" required="required" />
	<input type="password" name="password" placeholder="패스워드" required="required" />
	<input type="email" name="email" placeholder="이메일" required="required" />
	<input type="text" name="name" placeholder="이름" required="required" />
	<button>가입</button>
</form>

1-1 . web 패키지 생성 > dto 패키지 생성 > auth 패키지 생성 > SingupDto(username,password,email,name) 클래스 생성

//SingupDto
@Data //Getter,Setter
public class SingupDto {
    private String username;
    private String password;
    private String email;
    private String name;
}

DTO ?

Data Transfer Object : 통신할 때 필요한 데이터를 담아주는 object

@Data ?

Getter와 Setter를 만들어주는 어노테이션

1-2. AuthController에서 데이터(SignupDto) 받아오기

Form으로 데이터가 날아오면 key=value 형식으로 데이터를 받아옴

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

궁금증 (1)

signup.jsp 에서 username, pw, email, name을 입력시 이 데이터들을 Dto에 담는 것까지 ok
그리고 이 Dto를 파라미터로 받는다.

그렇다면 누가 jsp 파일에서 입력된 데이터를 Dto에 담아주는건데?

jsp 폼 데이터의 확인 버튼을 클릭하면 Post 요청이 발생,
@PostMapping("/auth/signup")와 일치하는 엔드포인트로 보내짐

엔드포인트로 보내졌을 때, 해당 엔드포인트를 처리하는 메서드인 signup(SignupDto signupDto)가 호출됨
이 메서드가 POST 요청 처리하고, jsp 폼에 입력된 데이터 (즉 엔드포인트로 전송된 데이터)는 SignupDto 객체로 자동 매핑된다

HTML 폼의 각 입력 필드(username, password, email, name)에는 name 속성이 지정되어 있고,
이 name 속성은 서버로 데이터를 전달할 때 해당 데이터의 식별자 역할을 한다.
이 name 속성과 SignupDto 클래스의 필드 이름이 일치하면 Spring Framework가 요청 데이터를 자동으로 해당 필드에 매핑합니다.

@PostMapping?
post 요청이 "~" < 엔드포인트로 전달될 때 호출
(+)
사용자가 JSP 페이지에서 "확인" 버튼을 클릭하면 HTML 폼이 서버로 데이터를 전송하는 POST 요청을 생성합니다.

POST 요청의 action 속성에 /auth/signup와 같은 경로가 지정되어 있으므로, 이 요청은 Spring 컨트롤러의 @PostMapping("/auth/signup")와 일치하는 엔드포인트로 보내집니다.

Spring 컨트롤러에서는 해당 엔드포인트를 처리하는 메서드인 signup(SignupDto signupDto)가 호출됩니다. 이 메서드는 POST 요청을 처리하고, 폼에서 전송된 데이터는 SignupDto 객체로 자동 매핑됩니다.

따라서 "확인" 버튼을 누르면 POST 요청이 서버로 보내지고, 그 결과로 @PostMapping("/auth/signup") 엔드포인트에 매핑된 컨트롤러 메서드가 실행됩니다.

1-3. 데이터베이스에 Insert 해주기 : Model 필요

domain 패키지 생성 > User 패키지 생성 > User.java 클래스 생성

JPA ?
Java Persistence API
자바로 데이터를 영구적으로 저장(DB)할 수 있는 API를 제공

package com.cos.photogramstart.domain.User;

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

import javax.persistence.*;
import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라감
    private int id;
    private String username;
    private String password;
    private String name;
    private String website; // 웹사이트
    private String bio; // 자기소개
    private String email;
    private String phone;


    private String profileImageUrl; // 사진
    private String role; // 권한

    private LocalDateTime createDate;

    @PrePersist // 디비에 INSERT 되기 직전에 실행
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }

}

LocalDateTime ?
import java.time.LocalDateTime;
데이터가 언제 들어왔는지
@PrePersist
사용하면 id, name .. 등의 데이터를 넣을 때 자동으로 LocalDateTime값이 들어감
@Entity
DB에 만들어지기 위해서 필요한 어노테이션
디비에 테이블을 생성
@Id
PK가 필요하기 때문에 id에 추가해주어야 하는 어노테이션
@GeneratedValue(strategy = GenerationType.IDENTITY)
번호 증가 전략이 데이터베이스를 따라감
@NoArgsConstructor
빈생성자
@AllArgsConstructor
전체 생성자

여기까지 해보고 mysql bench 들어가서 아래와 같이 입력하면 테이블이 생성되어 있는 것을 확인할 수 있다.

1-4. signupDto에서 받은 데이터를 User라는 Object에 담아서 DB에 넣기

Dto에서 받은 데이터를 객체에 담는 가장 좋은 방법이 @Bulider 어노테이션을 사용하는 것
빌더 어노테이션 참고 : https://pamyferret.tistory.com/67

User.java에서 @Builder 추가

//SignupDto
 public User toEntity(){
        return User.builder()
                .username(username)
                .password(password)
                .email(email)
                .name(name)
                .build();
    }

잘 담겨졌는지 확인

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

이제 잘 담긴 이 데이터 객체를 db에 넣어보자
domain> user > UserRepositroy 인터페이스 생성

public interface UserRepository extends JpaRepository<User, Integer> {
}

<User, Integer> = <오브젝트 이름, 프라이머리 키 타입>
JpaRepository을 상속했으니 어노테이션이 없어도 자동으로 Ioc 등록이 됨

Service 계층 생성
service 패키지 생성 > AuthService 클래스 생성

@RequiredArgsConstructor
@Service //1.Ioc 2.트랜잭션 관리:  이 서비스 어노테이션에 등록된 객체를 관리해줌
public class AuthService {

    private final UserRepository userRepository;
    public User 회원가입(User user){
        //회원가입 진행
       User userEntity = userRepository.save(user); //save가 된 뒤에 (즉 db에 들어간 후에) 응답받음
        // user: 외부 통신에서 받은 것 userEntity : 데이터 베이스에 있는 user오브젝트를 담은것
        return userEntity;
    }
}

이후 AuthController에 적용시키기

//AuthController
 @PostMapping("/auth/signup")
    public String signup(SignupDto signupDto)
        User user = signupDto.toEntity();
        User userEntity = authService.회원가입(user);
        return "auth/signin";
    }

@DI 하는법

  • @Autowired
  • @RequiredArgsConstructor + private final UserRepository userRepository;

여기까지 정리하자면,
1. Auth controller 생성 > GetMapping/PostMapping 설정 > SignupDto > Model : User > UserRepository > Service

2. 비밀번호 해시

SecurityConfig 를 수정해보자

@EnableWebSecurity
해당 파일로 시큐리티 활성화시킨다

시큐리티 활성화 어노테이션을 추가해준다.

@Bean //SecurityConfig가 Ioc에 등록될때, Bean 어노테이션을 읽어서 리턴한 BCryptPasswordEncoder를 Ioc가 들고있음 > DI해서 쓰기만 하면 됨
    public BCryptPasswordEncoder encode(){
        return new BCryptPasswordEncoder();
    }

@Bean
IoC 컨테이너가 관리하는 객체
https://ittrue.tistory.com/221

//AuthService
 private final BCryptPasswordEncoder bCryptPasswordEncoder;
 
  String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword);
        user.setRole("ROLE_USER"); // 관리자는 ROLE_ADMIN

3. username 중복 방지

//User
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라감
    private int id;

    @Column(unique = true)
    private String username;
    ...

@Column(unique = true)
unique로 중복되는 값이 저장되지 않는다.

4. id 길이 제한

위의 User 객체에서 length = 20 를 추가하면 바로 적용이 되나?

  @Column(length = 20, unique = true)

그렇지 않다. 쿼리가 변경된 것이니 application에서 update > create 변경 후 다시 적용하기

5. AOP / username 중복방지 : 후처리, id 길이 제한 : 전처리

전처리 : validation 유효성검사
후처리 : exception handler

이 기능들을 공통 기능 이라고 한다.
없어도 잘 돌아가지만, 있어야 더 깔끔하게 돌아간다.

이를 AOP 라고 부른다.
전처리 후처리는 파일을 따로 두어 처리한다. 아니면 코드 너무 조잡해짐

[전처리]

//pom.xml 추가
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
<!--			<version>2.4.4</version>-->
		</dependency>

사용할 객체에 @Valid 어노테이션 추가

//AuthController
  public String signup(@Valid SignupDto signupDto, BindingResult bindingResult){

        if (bindingResult.hasErrors()){
            Map<String,String> errorMap = new HashMap<>();
            for(FieldError error:bindingResult.getFieldErrors()){
                errorMap.put(error.getField(), error.getDefaultMessage());
                log.info(error.getDefaultMessage().toString());
            }
        }
//SingupDto
public class SignupDto {
   @Max(20)
   private String username;
   @NotBlank
   private String password;
   @NotBlank
   private String email;
   @NotBlank
   private String name;

public String signup(Valid SignupDto signupDto, BindingResult bindingResult)

SignupDto signupDto 를 Validation 체크해서
문제가 생기면 = ","
발생한 오류를 모두 BindingResult bindingResult에 담아둔다. 담아둔게 FieldError

profile
차곡차곡

0개의 댓글