ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링부트+JPA - 인스타그램 클론 코딩 03 - 1 시큐리티 및 회원가입
    스프링/스프링부트+JPA - 인스타 2021. 9. 17. 19:23

    이지업 최주호 강사님 강의 참조

     

     

    01 시큐리티 기본 세팅

    • 처음 테스트 당시 시큐리티 메인 페이지(/login)가 첫번째 순서로 나오는데 그 이유는 프로젝트 세팅에 시큐리티가 담겨 있기 때문이다. 즉 이 시큐리티 설정을 통해 인증과 권한을 나눠야 한다.

     

    ① 시큐리티 구조

    ② 시큐리티 기본 뼈대 둘러보기

    @EnableWebSecurity 
    @Configuration 
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		// TODO Auto-generated method stub
    		super.configure(http);
    	}
    }
    • @EnableWebSecurity - 현재 이 클래스 파일로 시큐리티를 활성화 시킴(스프링 부트가 더이상 인증 작업 하지 않음)
    • @Configuration - IoC 등록
    • configure 오버라이딩을 하면 super.... 이 나오는데 이것을 지우게 되면 기존 스프링 시큐리티가 가지고 있는 기능 모두가 비활성화 됨
    • 스프링 시큐리티 클래스 작성시에는 반드시 WebSecurityConfigurerAdapter/@EnableWebSecurity/@Configuration 이 따라옴

    더 이상 스프링 부트의 시큐리티 로그인 화면이 나오지 않는다!

     

     

     

    ③ 직접 인증 설정

    @EnableWebSecurity 
    @Configuration 
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.authorizeRequests()
    		.antMatchers("/","/user/**","/image/**","/subscribe/**","/comment/**")
    		.authenticated() 
    		.anyRequest().permitAll()
    		.and() 
    		.formLogin()
    		.loginPage("/auth/signin")
    		.defaultSuccessUrl("/"); 
    	}
    }
    • HttpSecurity가 들고있는 .authorizeRequests()를 통해 인증을 요청 시킴
    • .antMatchers("") - 이하 주소는 인증 요청 받음
    • .anyRequest().permitAll() - 그외 주소는 인증 없이 갈 수 있음
    • .and().formLogin().loginPage("/auth/signin") - 인증 페이지 요청이 오면 인증 페이지로 이동("/auth/signin")
    • 인증 성공하면 "/" 으로 이동

    인증 받아야 하는 주소를 요청하면 REDIRECT로 인증부터 하는 페이지로 강제 이동 시킴! 

     

    • 시큐리티 설정이 끝난것이 아니다. 하나씩 하나씩 기능을 만들어가면서 시큐리티를 완성 시킬 예정

     

     

     

     

    02 회원가입 구현

    • 기본적인 디자인은 다 세팅이 되어 있다

    • 가입하기 누른 후 데이터 입력을 하고 가입을 누르면 처리되도록 만들어 보자

     

    ① signup.jsp에 데이터를 받도록 회원가입 코드 수정(action/method를 통해 데이터를 들고 가입 처리 됨)

    <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차 세팅 

    @Controller
    public class AuthController {
    	
    	private static final Logger log = LoggerFactory.getLogger(AuthController.class);
    	
    	@GetMapping("/auth/signin")
    	public String signinForm() {
    		return "auth/signin";
    	}
    	
    	@GetMapping("/auth/signup")
    	public String signupForm() {
    		return "auth/signup";
    	}
    	
    	//가입버튼누르면 -> /auth/signup -> /auth/signin
    	@PostMapping("/auth/signup")
    	public String signup(SignupDto signupDto) {
    		return "auth/signin";
    	}
    }
    • 여기서 테스트를 해보면 가입 버튼을 눌러도 '/auth/signin'으로 넘어가지 않는다. 왜? FORM형식의 데이터 송수신은 CORS 토큰이 걸려있기 때문이다

    ※CORS(CSRF토큰)란?

    현재 프로젝트는 시큐리티 기반이기때문에 이 프로젝트는 시큐리티로 감싸져 있는 형태인데 어떠한 요청이오면 이 시큐리티가 CSRF토큰을 1차로 검사를 하게 된다. 예로 클라이언트가 회원가입창을 요청하면 우리 서버는 'signup.jsp'를 돌려주면서 CSRF토큰을 심어서 주게되고 클라이언트는 응답 받은 페이지로 데이터를 INPUT태그에 넣어서 회원가입 요청을 보내면 이 태그들에 CSRF토큰이 붙여지게 된다. 이때 시큐리티가 자기가 줬던 토큰이 달린 페이지와 클라언트가 요청하는 페이지가 같은 토큰을 가지고 있는지 검사 하게 되는 것.(=> 즉 인증을 거쳐야한다는 말)

    우리는 프로젝트상 CSRF토큰을 비활성화하고 진행한다 왜냐하면 JS요청도 힘들고 많은 제약이 걸리기 때문.

     

    • 시큐리티에서 비활성화 코드를 추가하자
    @EnableWebSecurity 
    @Configuration 
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.csrf().disable();
            ..
        }
    }

    → 테스트를 해보면 가입하기버튼 클릭시 잘 넘어간다

     

     

     

    ③ 데이터 받기위한 DTO 생성

    ※DTO란?

    Data Transfer Object의 약자로 계층간 데이터 교환을 위한 객체. VO라고도 불림. 요청/응답 둘다 DTO를 가지고 있고 이 DTO들은 요청할 양식-응답할 양식을 정의한 것이라고 보면 쉬움

    • signup.jsp 에 나와있는 input 데이터를 그대로 넣어 DTO를 만든다
    @Data
    public class SignupDto {
    	private String username;
    	private String password;
    	private String email;
    	private String name;
    }

    → DTO 테스트 (회원가입 해보자)

    	//가입버튼누르면 -> /auth/signup -> /auth/signin
    	@PostMapping("/auth/signup")
    	public String signup(SignupDto signupDto) {
    		log.info(signupDto.toString());
    		return "auth/signin";
    	}

    굿!

    • 데이터가 넘어오는이유는 저번에 강의를 들었던 리플렉션을 생각해보면 된다. 매핑 + 리플렉션 파라미터받기로 인해 컨트롤러를 알아서 찾아가게되고 그 파라미터에 맞는값을 정확히 찾을 수 있게 되는 것!
      (리플렉션 강의에서 setData 부분을 보면 이해가 더 쉽게 된다!)

     

     

     

     

    ④ DB와 연결할 모델 세팅

    @Builder
    @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 gender;
    	private String profileImageUrl;
    	private String role;
    	
    	private LocalDateTime createDate;
    	
    	@PrePersist
    	public void createDate() {
    		this.createDate = LocalDateTime.now();
    	}
    }
    • @PrePersist - 이 어노테이션이 정의된 메소드는 Entity가 DB에 저장되기 전 실행 됨
    • 데이터가 넘어오는것을 확인했으니 DTO에 세팅된 값과 모델값을 맞춰서 DB에 넣어보자 

    → SignupDto 수정

    @Data
    public class SignupDto {
    	private String username;
    	private String password;
    	private String email;
    	private String name;
    	
    	public User toEntity() {
    		return User.builder()
    				.username(username)
    				.password(password)
    				.email(email)
    				.name(name)
    				.build();
    	}
    }

    * 빌더 패턴을 쓰는 이유(https://mangkyu.tistory.com/163)

    → 재 테스트

     

     

    • DB와 데이터 송수신을 하기 위해서는 Service와 Repository가 필요

    → UserRepository

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

    → AuthController

    @RequiredArgsConstructor
    @Controller
    public class AuthController {
    	private final AuthService authService;
        ..
    	//회원가입요청
    	@PostMapping("/auth/signup")
    	public String signup(SignupDto signupDto) {
    		User user = signupDto.toEntity();
    		User userEntity = authService.join(user);
    		System.out.println(userEntity);
    		return "auth/signin";
    	}
    }
    • @Autowired로 의존성을 주입할 수 있지만 final을 붙여서 @RequriedArgsConstructor의 기능인 final이 붙은 변수를 스프링에서 관리하도록 만들어주는것이 더 좋다
    • 예전에는 @Autowired를 써서 더 간단히 했지만 이 방법은 편리한거 이 외에 장점이 없다. 추전되지 않는다. 이유는 아래와 같다
      1) 스프링팀에서 생성자 주입을 권장하고 있음
      2) 생성자 주입으로 따라오는 장점은 순환 참조 방지
      3) 테스트 코드 작성 용이(Junit)
      4) 코드 악취 제거
      5) 객체 변이 방지(Final로 상수로 묶음)
      이 블로그를 참조 하자(https://upcake.tistory.com/417?category=986315)

     

    → AuthService

    @RequiredArgsConstructor
    @Service
    public class AuthService {
    	
    	private final UserRepository userRepository;
    	
    	//회원 가입 진행
    	@Transactional
    	public User join(User user) {
    		User userEntity = userRepository.save(user);
    		return userEntity;
    	}
    }
    • User user 는 외부로부터 들어오는 데이터, User userEntity 는 save가 된 뒤의 데이터인 user를 저장하는 것
    • @Transactional - 트랜잭션의 오류/범위 등을 스프링에서 관리하도록 만듬
    더보기

    트랜잭션은 DB의 상태를 변화시키는 하나의 작업 단위. @Transactional이 붙으면 연산과정이 이루어 질때 다른 연산이 끼어 들 수 없으며, 오류가 생기면 원래대로 되돌리거나 성공하면 결과를 반영하게 된다.
    @Transactional은 INSERT/UPDATE/DELETE 시에만 쓴다

    → 테스트

    굿!

    • 여기서 문제는 DB의 암호가 그대로 보인다는 것! 이를 해결해보자

     

     

     

     

     

    03 비밀번호 암호화 처리 및 권한 설정

     

    ① SecurityConfig에 암호화 빈 등록

    @EnableWebSecurity 
    @Configuration 
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	
    	@Bean
    	public BCryptPasswordEncoder encode() {
    		return new BCryptPasswordEncoder();
    	}
        ...

     

    ② AuthService에 회원가입시 암호화 되도록 설정

    @RequiredArgsConstructor
    @Service
    public class AuthService {
    	
    	private final UserRepository userRepository;
    	private final BCryptPasswordEncoder bCryptPasswordEncoder;
    	
    	//회원 가입 진행
    	@Transactional
    	public User join(User user) {
    		String rawPassword = user.getPassword();
    		String encPassword = bCryptPasswordEncoder.encode(rawPassword);
    		user.setPassword(encPassword);
    		user.setRole("ROLE_USER");
    		User userEntity = userRepository.save(user);
    		return userEntity;
    	}
    }

    → 테스트

    • 정상적으로 처리는 되나 2가지의 문제가 있다 1)중복회원가입 2)아이디글자수 제한 없음. 해결해보자

     

     

     

     

     

     

     

     

Designed by Tistory.