스프링 회원가입(2)

2023. 10. 23. 19:37Springboot

1. Requirements

  • Spring boot version 2.4.5  

 2. Stacks

  Springboot

  • Loombook
  • Spring Data JPA 
  • Spring Boot DevTools
  • Spring Security
  • Spring Web
  • Spring boot version 2.4.5  

  Mybatis

 3. CSRF token

     이전 코드에는 csrf token을 비활성화하여 만들어져 웹 애플리케이션의 보안에서 문제점이 많았습니다.

     비활성화시에 대한 문제점입니다.

     1) CSRF 공격 위험: CSRF 공격은 악의적인 웹사이트가 사용자의 브라우저를 통해 인증된 세션을 사용하여 사용자의 동의 없이 서버에               요청을 보내는 공격입니다. CSRF 토큰을 사용하지 않으면 이러한 공격을 쉽게 수행할 수 있습니다.
     2) 사용자의 권한 남용: CSRF 공격을 통해 공격자는 사용자의 인증된 세션을 남용하여 서버의 보호된 기능을 실행할 수 있습니다.

           예를 들어, 사용자가 의도하지 않은 상태에서 계정 비밀번호를 변경하거나 중요한 데이터를 삭제할 수 있습니다.
           기백 엔드포인트 보호 미흡: CSRF 공격으로부터 보호되지 않은 애플리케이션은 사용자와 관련된 중요한 데이터 또는 작업을 변경하는 백엔드 엔드포인트에 노출됩니다. 이로 인해 민감한 데이터의 무단 수정 가능성이 증가하며, 데이터의 무단 삭제 또는 수정이 발생할 수 있습니다.
     3) 보안 위반 및 개인 정보 노출: CSRF 공격으로 공격자가 사용자의 인증 정보를 훔칠 수 있는 상황이 발생할 수 있습니다.

          예를 들어, 사용자의 세션 쿠키를 이용하여 공격자가 사용자 계정에 로그인할 수 있으므로, 민감한 정보에 액세스할 수 있습니다.
     4) 신뢰성 하락: CSRF 공격으로 인해 사용자가 애플리케이션의 신뢰성을 상실할 수 있으며,

          사용자는 애플리케이션을 사용하지 않거나 신뢰하지 않게 될 수 있습니다.

 

     이에 대한 해결점과 링크입니다.

     

         1. signin.jsp

<!-- 수많은 코드들...-->
<!-- form만 가져왔습니다-->
<!--로그인 인풋-->
<form class="login__input" action="/auth/signin" method="post">
    <input type="text" name="username" placeholder="유저네임" required="required" />
    <input type="password" name="password" placeholder="비밀번호" required="required" />
    <button>로그인</button>
</form>
<!--로그인 인풋end-->

  https://blog.paimon.studio/46

 

[스프링 Security] CSRF 토큰 이야기 - 그래서 개발자는 뭘 하면 되죠

CSRF 토큰을 통한 보호 이 글은 Synchronizer Token Pattern에 기반한 CSRF 활용의 빠른 지침이다. Q1. 클라이언트는 어떻게 CSRF 토큰을 얻나요 방법1. 서버가 HTML 렌더링 시 meta 태그에 토큰 집어 넣어 주기

blog.paimon.studio

4. Code

       1. signin.jsp

           localhost:8080/auth/sigin으로 접속했을 때 나오는 첫 페이지

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test</title>
</head>

<body>
    <div class="container">
        <main class="loginMain">
        <!--로그인섹션-->
            <section class="login">
               <!--로그인박스-->
                <article class="login__form__container">
                   <!--로그인 폼-->
                   <div class="login__form">
                        <h1><img src="/tmp.jpg" alt=""></h1>
                        
                        <!--로그인 인풋-->
                        <!-- 수많은 코드들...-->
                        <form class="login__input" action="/auth/signin" method="post">
                            <input type="text" name="username" placeholder="유저네임" required="required" />
                            <input type="password" name="password" placeholder="비밀번호" required="required" />
                            <button>로그인</button>
                        </form>
                        <!--로그인 인풋end-->
                        
                        <!-- 또는 -->
                        <div class="login__horizon">
                            <div class="br"></div>
                            <div class="or">또는</div>
                            <div class="br"></div>
                        </div>
                        <!-- 또는end -->
                        
                        <!-- Oauth 소셜로그인 -->
                        <div class="login__facebook">
                            <button>
                                <i class="fab fa-facebook-square"></i>
                                <span>Facebook으로 로그인</span>
                            </button>
                        </div>
                        <!-- Oauth 소셜로그인end -->
                    </div>
                    
                    <!--계정이 없으신가요?-->
                    <div class="login__register">
                        <span>계정이 없으신가요?</span>
                        <a href="/auth/signup">가입하기</a>
                    </div>
                    <!--계정이 없으신가요?end-->
                </article>
            </section>
        </main>
        
    </div>
</body>

</html>

 

       2. signup.jsp

           - localhost:8080/auth/signup

           - auth/signin의 회원가입 버튼을 누를 시 나오는 페이지       

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test</title>
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
        integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />
</head>

<body>
    <div class="container">
        <main class="loginMain">
           <!--회원가입섹션-->
            <section class="login">
                <article class="login__form__container">
                  
                   <!--회원가입 폼-->
                    <div class="login__form">
                        <!--로고-->
                        <h1><img src="/images/tmp.jpg" alt=""></h1>
                         <!--로고end-->
                         
                         <!--회원가입 인풋-->
                        <form class="login__input" action="/auth/signup" method = "post">
                            <input type="text" name="userId" placeholder="유저네임" required="required" maxlength = "30"/>
                            <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>
                        <!--회원가입 인풋end-->
                    </div>
                    <!--회원가입 폼end-->
                    
                    <!--계정이 있으신가요?-->
                    <div class="login__register">
                        <span>계정이 있으신가요?</span>
                        <a href="/auth/signin">로그인</a>
                    </div>
                    <!--계정이 있으신가요?end-->
                    
                </article>
            </section>
        </main>
    </div>
</body>

</html>

 

      3. application.yml

          - springboot를 실행할 때 기본적으로 필요한 요소를 적음

          - datasource는 mariadb를 직접 지정해야 사용할 수 있음

server:
  port: 8080(자신이 7070으로 설정했다면 localhost:7070으로 들어가야함)				
  servlet:
    context-path: /
    encoding:
      charset: utf-8
      enabled: true
    
spring:
  mvc:
    view:
      prefix: (src/main밑에 있는 폴더명에 저장)
      suffix: .jsp
      
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:(mybatis 연결포트)/(자신이 연결한 데이터베이스 이름)?serverTimezone=Asia/Seoul
    username: (자신이 만든 아이디)
    password: (자신이 만든 비밀번호)
    
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
      
  servlet:
    multipart:
      enabled: true
      max-file-size: 2MB

  security:
    user:
      name: test
      password: 1234

 

       4. AuthController.java

          - @Controller : *. jsp페이지를 직접 받는 컨트롤러, 주로 파일을 리턴 받습니다.

          - @RequiredArgsConstrutor : private final키워드의 생성자를 사용할 수 있게 합니다.

          - 화면을 직접 받으며 @Valid와 BindingResult를 사용하여 유효성검사를 시행합니다.

          - 회원가입을 위해 유효성검사를 마친 signupDto를 service의 비즈니스 로직 수행을 user객체를 보냅니다.

package com.pjh.web;

import java.util.HashMap;
import java.util.Map;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.pjh.domain.user.User;
import com.pjh.service.AuthService;
import com.pjh.web.dto.SignupDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class AuthController {
	private final AuthService authService;

	@GetMapping("/auth/signin")
	public String login() {
		return "/auth/signin";
	}
	@GetMapping("/auth/signup")
	public String join() {
		return "/auth/signup";
	}
	@PostMapping("/auth/join")
	public String login(
			@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());
			}
			//throw new CustomValidationApiException("sorry", errorMap);
		}
		authService.회원가입(signupDto.toEntity());
		return "/auth/signin";
	}
	
	/*로그인 페이지 <%include ../layout/*.jsp%> 설정 시에 spring dependencies 설정에 security tag-libs 설정 후 재부팅해야함 */
}

        5. SignupDto.java

             - signup.jsp에서 받는 form의 필드들을 전달받아 service로 보내는 형식을 가집니다.

             - toEntity()의 builder(). build() 형태를 통해 순서가 뒤 바뀌어도 유저 객체에 전달되게 합니다.

package com.pjh.web.dto;

import javax.validation.constraints.NotBlank;

import com.pjh.domain.user.User;

import lombok.Data;

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

 

       6. AuthService.java

             - @Service 비즈니스 로직을 구현을 위해 생성하는 service 어노테이션입니다.

             - @Transactional DB에 트랜잭션(일의 최소 단위) 처리를 springboot에서 구현한 형태로 수행할 수 있게 합니다.

             - BCrytPasswordEncoder : DB에 저장된 패스워드를 bcryt 화하여 저장합니다.

               패스워드를 암호화해 주는 메서드입니다. encde() 메서드는 SHA-1,

               8바이트로 결합된 해쉬, 랜덤 하게 생성된 솔트(salt)를 지원합니다. 

               ex) 비밀번호 : 1234 -> db에 저장되는 비밀번호 :

                    $2a$10$0i10aVWcB9SDN6UYT2hav.m3phbL6rFhlqSKTIIHsqyoFmfHdrGbC

 

package com.pjh.service;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.pjh.domain.RoleType;
import com.pjh.domain.user.User;
import com.pjh.domain.user.UserRepository;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AuthService {
	
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCrytBCryptPasswordEncoder;
	
	@Transactional
	// 트랜잭션 
	// ex) 송금 서비스 a가 b에게 송금 (a_db update)
	// b의 금액이 변화해야함 (d_db update)
	// a_db update와 b_db update 두가지 일을 한가지 일로 묶어서 진행하며
	// 이를 하기 전에 아무도 건들지 말라는 표시
	public User 회원가입(User user) {
		User userEntity = userRepository.save(user);
		// userRepository에 user 객체를 저장
		String rawPassword = user.getPassword();
		System.out.println(user.getEmail()+" " +user.getPassword()+" "+user.getUsername());
		String bCrytPassword = bCrytBCryptPasswordEncoder.encode(rawPassword);
		
		user.setPassword(bCrytPassword);
		user.setRole(RoleType.USER);
		
		return userEntity;
		// userEntity는 db에 최종적으로 저장한 형태를 가져오는 것이 아닌 이전에 저장한 signupDto의 4가지 형태를 저장한 것을 반환
	}
}

 

        7. UserRepository

            - DB와 직접 맞닿아 있는 인터페이스입니다.

            - DB에 처리할 SQL문을 직접 사용할 수 있고 extends 하여 JpaRepository의 crud형태의 기본적인 연산이 지원됩니다.

package com.pjh.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

// DAO
// 자동으로 Bean 등록
public interface UserRepository extends JpaRepository<User, Integer>{
	// table User을 관리하는 인터페이스, PK는 integer 
	User findByUsername(String username);
}

 

         8. SecurityConfig.java

            - spring security부분에 필요한 필수 요소입니다.

            - SecurityConfig부분이 없다면 spring security에서 localhost:8080로 가려고 한다면 redirect를 localhost:8080/login으로

              보냅니다. 이는 앞서 올린 3. 구상 도을 보시면 됩니다.

            - csrf 토큰 : 비정상적인 접근에 대한 처리를 막기 위해 만든 spring security입니다.

              http.csrf.disable() 추후 수정예정 -> 유효성 검사면에 안 좋음 

package com.pjh.config;

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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity			// 해당 파일로 시큐리티를 활성화
@Configuration				// IoC     
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	@Bean
	public BCryptPasswordEncoder encode() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// super() 삭제 -> 기존의 시큐리티가 가지고 있는 기능이 다 비활성화되어짐
		// http.csrf().disable();
		// csrf 비활성화
		http.authorizeRequests()
		.antMatchers("/","/main/**").permitAll()
		// 조금있다가 바꿔줄 예정
		//.antMatchers("/","/user/**","/image/**","./subscribe/**","/commit/**","/api/**").authenticated()
		.anyRequest().permitAll()
		.and()
		.formLogin()
		.loginPage("/auth/login")				// get 방식 활용시
		.loginProcessingUrl("/auth/login")		// post 방식 활용시 spring security가 자동으로 함, loginController를 따로 안만듦
		.defaultSuccessUrl("/auth/main");		// 성공시에 /auth/main으로 이동
		// 처음에 개발할때는 .antMatchers("/","/user/**...") 제외하기
		// authenticated() -> 인증 필요
		// anyRequest().permitAll() -> 인증이 필요없는 것은 모두 허용한다.
		// and() -> 그리고
		// formLogin().loginPage("/") -> 폼로그인 페이지인  "/~~"쪽으로 강제 이동
		// 성공한다면 default로 "/"쪽으로 이동
	}
}

 

         9. PrincipalDetails.java

            - spring security부분에 세션을 저장하는 방법입니다.

            - 사용자가 로그인을 할 때의 controller에서 service부분에서 비즈니스 로직을 실행하지 않고 principalDetails의

              세션을 사용하여 로그인을 하게 됩니다.

PrincipalDetails

package com.pjh.config.auth;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.pjh.domain.user.User;

import lombok.Data;

@Data
public class PrincipalDetails implements UserDetails {

	private static final long serialVersionUID = 1L;
	
	private User user;
	
	public PrincipalDetails(User user) {
		this.user=user;
		// 세션 처리
	}
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// 권한을 가져오는 함수 -> user-role
		// ex) 사람당 0~수많은 숫자의 권한을 다양하게 가질 수 있기 때문에 collection타입으로 리턴
		Collection<GrantedAuthority> collector = new ArrayList<>();
		collector.add(()-> {
			return user.getRole();
		});
		return collector;
	}

	@Override
	public String getPassword() {
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {
		// 계정이 만료되었는지
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		// 계정이 잠겼는지
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		// 비밀번호 긴 기간동안 안바뀌었는지
		return true;
	}

	@Override
	public boolean isEnabled() {
		// 긴기간동안 계정 활성화가 안되었는지
		return true;
	}
}

 

 

      10. PrincipalDetailsService.java

            - spring security부분에 세션을 저장하는 방법입니다.

package com.pjh.config.auth;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.pjh.domain.user.User;
import com.pjh.domain.user.UserRepository;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService{
	
	private final UserRepository userRepository;
	
	// 1. 패스워드는 알아서 스프링 시큐리티가 판단하기 때문에 판단을 안해도 됨.
	// 2. 리턴이 잘되면 자동으로 UserDetails 타입을 세션을 만듦.
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User userEntity = userRepository.findByUsername(username);
		//System.out.println("==============실행!============");
		if(userEntity == null) {
			return null;
		}else {
			// System.out.println("userEntity is null? : "+userEntity.getUsername());
			return new PrincipalDetails(userEntity);
			// 세션에 저장될때 userEntity의 정보를 가지고 있음.
		}
	}
}

 

 

 

'Springboot' 카테고리의 다른 글

스프링 이미지 업로드(2)  (0) 2023.11.07
스프링 메모장  (1) 2023.11.01
스프링 이미지 업로드(1)  (1) 2023.10.30
스프링 회원 수정  (1) 2023.10.23
스프링 회원가입(1)  (2) 2023.10.20