본문 바로가기
Project/🥑JPA + SpringSecurity 게시판

[JPA + SpringSecurity 게시판] #12 Spring Security 회원가입

by 로띠 2022. 6. 21.

 


 

Spring Security는

Spring에서 제공하는 강력한 보안기능으로 인증과 인가에 따라 접근을 제어

 

 

✅ 인증

  • 본인확인 절차 (=로그인)
  • Home 접근은 누구나 가능하지만 글을 작성할 땐 로그인 된 사용자 인증 필요

✅ 인가

  • 인증된 사용자들 마다의 권한 등급 부여
    • 인증된 자에게 Role을 부여해 권한을 관리 ‘User’,’Admin’ 등
    • 네이버 카페를 이용하다 보면 회원등급에 따라 카테고리를 다르게 이용하는 그런 기능
      그래서 열심히 댓글달고 출첵해서 등업신청했던 기억이,,, 🤭

 

 

스프링 시큐리티 설정 시

모든 요청은 인증을 요구하게 된다

 

 

 미인증자의 요청 시 ‘/login’ redirect

 

  • 로그인, 로그아웃 페이지는 Spring Security 기본 제공
      • 기본 ID도 하나 제공되는데
      • id는 user / 비밀번호는 app실행 시마다 콘솔 창에 띄워진다

 

  • application.properties 에서 기본id 설정도 가능
💡 spring.security.user.name=user spring.security.user.password=1234
  • 홈 화면 등 인증 없이 접근 가능한 페이지는 아래에서 SecurityFilterChain 코딩할 것

 

 

✅ 로그인 사용자의 인증과 권한

 

  • 로그인 된 사용자에겐 ‘ROLE’을 부여해( ’USER’,’MEMBER’ 등 ) 접근을 허용
  •  일부 페이지에는 인가방식으로 인증된 사용자 중 접근 권한을 부여
    • 그 경우 ‘ROLE’ 이 ‘ADMIN’, ‘VIP’ 등이 될 수 있겠다

 

 

 

🔒Spring Security 사용하기🔒


 

 

 

 [ Build.gradle ] dependencies 추가

 
💡    implementation 'org.springframework.boot:spring-boot-starter-security’
        implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5’
 

 

  1. Spring Security 사용을 위한 의존성 추가
  1. Thymeleaf에서 Spring Security 웹 연동을 위한 의존성 추가
    • 사용자의 이름, 권한, 인증 여부 등에 따른 view 설정 가능
      • <sec:authentication="name"> 인증자 이름 반환
      • <sec:authorize="hasRole('ROLE_USER')"> ’ROLE’이 ‘USER’일 경우

 

 

 

이제 모든 페이지 접근에 인증을 요구 

 

security

의존성 추가 만으로도 이제 모든 요청에 인증을 요구한다 Wow

아직 권한과 예외설정을 하지 않아서

접근요청 시 Security가 제공하는 Login페이지로 이동한다

 

 

security 제공 ID

id : user

password : 콘솔창에 뜸

 

 

 

첫번째로 홈화면 등 인증을 요하지 않는 모두 공개 페이지들을 설정하고

두번째로는 회원가입을 구현해보자

 

 


 

🔒Spring Security 회원가입 만들기🔒

코딩시작 👩🏻‍💻

 

 


 

✅ 디렉터리 전체 구조

 

 

생성 목록

  1. [ config > SecurityConfig ]
  1. [ controller > MemberController ]
  1. [ domain > entity > Member ]
  1. [domain > repository > MemberRepository(interface) ]
  1. [ dto > MemberDto ]
  1. [ dto > Role(enum) ]
  1. [ service > MemberService ]

 

  1. html은 member 디렉토리 생성 후

[ template>member ] 안에 join.html 생성

 

 

보이는 순서대로 작성했는데

1번 SecurityConfig부터 만들어 보자

 

 


 

1. SecurityConfig

 

Spring Security 인증에 관한 설정 페이지

현재는 모든 요청에 인증을 요구하나

설정을 추가하면 인증을 요구하지 않는 페이지를 설정할 수 있다

 

@Configuration으로 설정 페이지 임을 명시하고

@Bean으로 객체를 등록한다

Security의 SecurityFilterChain을 이용해서 접근 허용과 제한을 관리할건데

먼저 코드를 보고 아래 설명을 이해하자

 

 

[ SecurityConfig ]

package com.rim.aboardcado.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
public class SecurityConfig  {

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests() // 요청에 대한 보안인증 체크 선언
                .mvcMatchers("/css/**","/image/**","/error").permitAll()
                .mvcMatchers("/","/join","/login","/search/**").permitAll()
                .mvcMatchers("/post").hasRole("USER")
                .anyRequest().authenticated() // 모든 요청에 체크 (권한 상관X)
        ;

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

 

✅ @Configuration

  • Configuration’은 ‘환경설정’이란 뜻으로 설정 역할을 하는 Class를 지정
  • 해당 class 하위에 @Bean을 사용하면 Bean등록 가능
    • Bean등록은 XML에도 가능하지만 @Configuration을 이용한 자바 설정파일을 이용하는 추세

 

✅ @Bean

  • Spring IoC 컨테이너가 관리하는 자바 객체를 빈(Bean)으로 사용자가 생성 및 등록
  • 메소드 레벨에서 선언하며, 반환되는 객체를 개발자가 수동으로 등록하는 @어노테이션
  • @Configuration으로 ComponentScan의 대상이 되기 때문에
  • @Component는 붙이지 않아도 된다

 

 

✅ SecurityFilterChain

 

Spring Security는 필터를 통해서 인증과 권한을 체크

 

👇아래 필터들이 순서대로 실행되면서

👇Spring Security에서 인증 및 권한부여관련 처리와 보안관련된 처리를 한다

 

👉아무것도 설정하지 않은 SecurityFilterChain기본 세팅 필터

더보기
  • WebAsyncManagerIntegrationFilter스프링웹과 SecurityContext간의 통합을 제공
  • SecurityContextPersistenceFilterSecurityContext 객체 생성 저장, 인증성공→ 세션에 저장
  • HeaderWriterFilter응답 헤더에 시큐리티 관련 헤더를 추가하는 필터
  • CsrfFiltercsrf공격을 방어하는 필터, form Post전송 시 사용
  • 모든 요청에 랜덤하게 생성된 csrf토큰을 요구→ 서버토큰과 비교, 일치해야 통과
  • LogoutFilter로그아웃관련 처리를 하는 필터
  • UsernamePasswordAuthenticationFilter아이디/비밀번호 로그인 시 인증 관련 필터
  • DefaultLoginPageGeneratingFilter기본 로그인 페이지 생성 필터
  • DefaultLogoutPageGeneratingFilter기본 로그아웃 페이지 생성필터
  • BasicAuthencationFilterHttpBasic 인증을 처리
  • RequestCacheAwareFilter현재 요청에 캐시 요청이 있는지 확인 및 처리
  • SecurityContextHolderAwareRequestFilter시큐리티 관련 메서드들을 스프링 시큐리티 기반으로 구현체를 연결
  • AnonymousAuthenticationFilterSecurityContext에 Authentication이 Null일경우 AnonymousAuthentication 생성
  • SessionManagementFilter세션관련 변조방지, 유효하지 않는 세션 체크, 동시성 제어, 세션생성전략 설정
  • ExceptionTranslationFilterAuthenticationException, AccessDeniedException 발생시 처리

 

  • FilterSecurityInterceptor마지막으로 실행되는 필터이며 리소스에 대한 접근이 가능한지 체크→ 처리

 

기본 세팅 필터들만 봐도 앞 서 봤던 security 제공 로그인, 로그아웃 기능과

앞으로 html에서 사용할 csrf 토큰 등 눈에 띄는 기능들이 보인다

ABoardcado는 이 기능들을 이용해서 회원가입, 로그인을 구현해볼 것이다

 

 

📈 충격번외 - 지금도 스프링은 변화하고 있다 📈

더보기

한 달전만해도,,

WebSecurityConfigurerAdepter을 상속받고 @EnableWebSecurity를 선언하면
SecurityFilterChain이 자동포함되어
@Override configure을 통해 보안설정을 커스터마이징해 썼다

 

하지만 두둥 ❗

Security 5.7.0 / springboot 2.7.0 이후 부터 이를 제공하지 않는다고 하니ㅠㅠ

👉 👉 원문 이동

아직 예제가 부족하지만 어차피 공부하고 연구하기로 한 일.

SecurityFilterChain으로 Develothy가 개척해보겟도다

 

 

✅ 코드설명

  • authorizeRequests()
    • 요청에 대한 보안인증 체크 선언
  • mvcMatchers("/css/**", "/js/**", "/image/**").permitAll()
    • 해당 URL에 대해서는 인증 요구X, 모두 허용(permit)
      • 미인증자도 css, 이미지 등 static resource은 공개
        • 로그인 전이라고 화면이 html 날것으로 공개되면 머쓱,,
      • “/’ 홈화면과 이제 만들 회원가입 “/join”도 허용해뒀다

 

  • mvcMatchers("/post").hasRole("USER")
    • 해당 URL에 대해서는 인증된 “USER” Role을 가지고 있어야 함
      • hasRole(”ADMIN”)이라면 “ADMIN” Role 필요
  • anyRequest().authenticated()
    • 열거 URL 외 모든 요청에 인증 체크 ( Role 상관X )

 

 

 

✅ BCryptPasswordEncoder

  • 비밀번호를 암호화 하기위한 Bean
  • 비밀번호를 DB에 그대로 저장하면, DB가 해킹당했을 때 회원정보가 그대로 노출된다
    BCryptPasswordEncoder  해시함수를 이용해 비밀번호를 암호화하여 저장

 

 


 

 

2. MemberDto

 

[ MemberDto ]

package com.rim.aboardcado.dto;

import lombok.Builder;
import lombok.Getter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;


@Getter
@Builder
public class MemberDto {

    private final PasswordEncoder passwordEncoder;

    private String name;
    private String email;
    private String password;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;

	// MemberDto -> Member
    public Member toEntity(MemberDto memberDto) {
        return Member.builder()
                .name(name)
                .password(passwordEncoder.encode(password))
                .email(email)
                .role(Role.USER)
                .build();
    }
}

 

✅ PasswordEncoder

  • password를 DB 저장 시에 암호화하여 저장
    • SecurityConfig에서 @Bean으로 등록했다

 

 

 


 

3. Role

package com.rim.aboardcado.dto;

public enum Role {
    USER, ADMIN
}

✅ Spring Security에서 ‘인가’에 해당하는 등급을 등록

✅ enum Type

 


 

 

4. Member (Entity)

 

package com.rim.aboardcado.domain.entity;

import com.rim.aboardcado.dto.Role;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

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

@Entity
@Table
@Getter
@Builder
@ToString
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id
    @Column(name = "member_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Column(unique = true)
    private String email;

    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;

}

 

 @Table

  • Entity와 매핑할 table. 즉 DB에서 schema 를 매핑
    • application.properties 에서 Data Source 설정했던 걸 기억하자
    • 🥑ABoardcado  update로 설정. DB테이블과 매핑 Entity를 비교해 변경사항 수정
  • class명과 동일하다면 생략 가능 (위 코드 생략가능)

 

 @Getter

  • 변수 값을 호출하는 getter() 메서드 자동 생성

 @Builder

  • 필드 초기화 작업을 도와줌
    • Setter 와 비슷한 역할 this.name = name; 의 기능으로 보면된다
  • 위 코드처럼 @NoArgsConstructor 로 생성된 생성자에 설정하는 @Builder 는해당 생성자를 사용하는 Builder만 생성되어, 의미있는 객채만 생성 (무분별X)

 

 @Entity

  • JPA가 관리하는 Entity. DB 테이블과 매핑
  • 생성자 필수
    • SpringBoot에서는 @Autowired, 빈 방식보다 생성자로 빈 주입을 권장

 

 @EntityListeners(AuditingEntityListener.class)

  • 해당 class에 Auditing 기능을 포함 선언
    JPA에서는 JPA Audit 을 제공하는데
    Audit 은 ‘감시하다’의 뜻으로 시간기록에 대해 자동으로 값을 생성
    @CreatedDate@LastModifiedDate의 기능을 사용하기 위해 선언
    ✅ JPA Auditing 활성화는 main class에서 @EnableJpaAuditing 추가 설정 ❗

 

 

 

 

 @NoArgsConstructor(access = AccessLevel.PROTECTED)

  • 파라미터가 없는 기본 생성자 생성
  • PROTECTED 무분별한 생성을 막기 위함

 

 @AllArgsConstructor

  • 모든 필드 값을 파라미터로 받는 생성자 생성
  • @Builder를 사용할 때, 생성한 생성자가 아무것도 없다면
    @AllArgsConstructor(access = AccessLevel.PACKAGE)가 자동 적용
      → 위 코드는 @NoArgsConstructor 기본생성자를 넣었기때문에 명시해줘야 함

 

 @Id

  • JPA Entity 객체의 식별자로 사용

 @GeneratedValue(strategy = GenerationType.IDENTITY)

  • 기본 key 생성을 DB에 위임 ( = AUTO_INCREMENT )
  • DB 마다 IDENTITY(MySQL), SEQUENCE  맞는 방식으로 해야하는데 AUTO는 알아서 적합배정

 

 @Column

  • 객체를 DB table의 컬럼에 매핑. nullable, unique, length 등 속성 입력가능
    • column 속성은 웹에서 유효성 검사를 해주지 않기 때문에 다음편에서 Validation 추가

 @CreatedDate, @LastModifiedDate

  • JPA에서 생성, 수정된 시간 정보 자동 생성

 

 

 


 

5. MemberRepository

package com.rim.aboardcado.domain.repository;

import com.rim.aboardcado.domain.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {

    Member findByEmail(String email);

}

 

 interface 생성, JpaRepository 상속

매핑할

< Entity, Id type >

Member 엔티티와 매핑하고 id의 타입인 Long 적어주기

 

✅ JPA를 상속하면 CRUD 메소드 지원

JPA로 회원가입, 수정, 탈퇴가 다 가능

 By 를 기준으로 Where절에 대응

메소드명을 해석해 처리하기 때문에 상속하면 끝 🙌

findById(id);findByEmail(email); 

 

 DB에서 email로 찾은 Member Entity 반환

 

 

 


 

6. MemberService

package com.rim.aboardcado.service;

import com.rim.aboardcado.domain.entity.Member;
import com.rim.aboardcado.domain.repository.MemberRepository;
import com.rim.aboardcado.dto.MemberDto;
import com.rim.aboardcado.dto.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    // 회원가입
		public Member saveMember(MemberDto memberDto) {

        // email 중복체크
        Member findMember = memberRepository.findByEmail(memberDto.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }

        return memberRepository.save(toEntity(memberDto));
    }

		// MemberDto -> Member
    public Member toEntity(MemberDto memberDto) {
        return Member.builder()
                .name(memberDto.getName())
                .password(passwordEncoder.encode(memberDto.getPassword()))
                .email(memberDto.getEmail())
                .role(Role.USER)
                .build();
    }
}

 

 findMember()

  • memberDto로 들어온 email로 DB에 data가 있는지 조회
    • 있으면 이미 가입된 이메일 alert

 

 toEntity

  • DB를 조작하는 Entity를 직접 사용하는 것은 여러 위험이 따르니
  • Dto를 두고 data를 변환해서 처리하는데
  • 마지막 저장은 toEntity로 Entity로 변환해 DB에 전달

 

 @Service

  • Service 비즈니스 로직임을 선언
  • Controller에서 로직을 호출하여 사용

 

 @AllArgsConstructor

  • 모든 필드값을 포함한 생성자 생성

 

 @Transaction

  • 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용
  • 실행, 종료(commit), 예외(rollback)작업 도중 오류가 발생했을 때 이전 모든 작업들이 성공하였더라도 작업 전 시점으로 되돌린다
  • class, method() 에 적용이 가능하고, method() (소분류)가 우선 적용
  • JPA Service에서 필수

 

 

 


 

7. MemberController

 

package com.rim.aboardcado.controller;

import com.rim.aboardcado.dto.MemberDto;
import com.rim.aboardcado.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;


@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/join")
    public String join( ) {
        return "member/join";
    }

    @PostMapping("/join")
    public String join(MemberDto memberDto) {

        memberService.saveMember(memberDto);
        return "redirect:/";
    }
}

✅ Controller는 Http 요청에 따른 Service로직과 View를 호출하는데

Board에서와 동일하게 save() JPA가 제공하는 기능이기 때문에 특별한 게 없다 😗

 

 

 


 

8. join.html

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container">
				<h1 style="color:#5a5a5a"> Hello:) I'm ABoardcado!</h1>
        <br>
        <h6 style="color:green"> <b>Create account</b></h6>
        <hr>

        <!--회원가입 폼-->
        <form action="/join" method="post">
            <div class="form-group row">
                <label for="inputName" class="col-sm-2 col-form-label"><strong>이름</strong></label>
                <div class="col-sm-10">
                    <input type="text" name="name" class="form-control" id="inputName" />
                </div>
            </div>
            <div class="form-group row">
                <label for="inputPassword" class="col-sm-2 col-form-label"><strong>비밀번호</strong></label>
                <div class="col-sm-10">
                    <input type="password" name="password" class="form-control" id="inputPassword" />
                </div>
            </div>
            <div class="form-group row">
                <label for="inputEmail" class="col-sm-2 col-form-label"><strong>Email</strong></label>
                <div class="col-sm-10">
                    <input type="text" name="email" class="form-control" id="inputEmail"></input>
                </div>
            </div>
            <div class="row">
                <div class="col-auto mr-auto"></div>
                <div class="col-auto">
                    <input class="btn btn-success" type="submit" role="button" value="signup" />
                    <input type="hidden" th:name="${_csrf.parameterName}"
                                            th:value="${_csrf.token}">
                    <a class="btn btn-danger" th:href="@{'/'}" role="button">취소</a>
                </div>
            </div>
        </form>
    </div>
</div>
</html>

🔐 CSRF ?

Cross Site Request Forgery

 

Spring Security를 사용할 경우 기본적으로 CSRF를 방어하기 위해 모든 method=”POST” 전송에는 CSRF토큰 값이 필요

 

CSRF 토큰은 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰.

사용자의 세션에 임의의 값을 저장해 요청마다 그 값을 포함하여 전송

  → 서버는 세션에 저장된 값과 요청이 온 값이 일치하는지 확인해 CSRF 공격을 방어

 

 CSRF공격

웹 어플리케이션 취약점 중 하나로 인터넷 사용자가 자신의 의지와 무관하게 해커가 의도한 행위(등록, 수정, 삭제 등)을 특정 웹사이트에 요청하게 만드는 공격

 

 CSRF 공격이 이루어지는 조건

  • 위조 요청을 전송하는 서비스에 (피해자가) 로그인한 상태
  • (피해자가) 해커가 만든 피싱 사이트에 접속

 

위 두가지 조건은 생각보다 쉽게 적용된다

우리는 카카오, 구글, 인스타 등 자동로그인을 해놓은 경우가 많고

피싱 사이트를 접속하지 않아도 해커가 XSS공격을 성공한 정상 사이트를 통해서도 공격이 된다

 

☝️ 예를 들어 위조된 toss사이트가 있다고 치면 ☝️

사용자는 toss인줄 알고 돈을 송금할 것이다

하지만 송금 폼에는 해커가 의도한 계좌번호가 적혀있고 그 곳으로 돈이 송금될 것이다

<form action="http://toss/api/sendMoney" method="post">
    <input type="hidden" name="accountNumber" value="해커가 의도한 계좌번호" />
		<input type="number" name="fake" value="계좌번호를 입력해주세요" />
    <input type="submit" value="송금하기"/>
</form>

 

이건 물론 막연한 예시지만 이런 식으로의 피해가 발생하고있다 (스미싱 문자, SNS 광고글 등)

이런 상황을 방지하게 위한 대표적 2가지 방법이있다.

 

 CSRF 공격 방어 방법

  1. Security Token 사용 (CSRF Token) (🥑ABoardcado 사용)
  1. Referrer 검증

 

일반 적으로 CSRF 공격 방어는 조회성(method=”GET”)에는 보통은 방어하지 않고

데이터 조작이 이루어지느 등록, 수정, 삭제(”POST”,”PATCH”,”DELETE”)에 적용

 

 

Referrer 검증

Back-end에서 request의 referrer를 확인해 domain ( *.toss.com )이 일치하는 지 검증하는 방법

일반적으로 referrer 검증만으로 대부분의 CSRF 공격을 방어할 수 있다.

 

하지만 같은 도메인 내의 페이지에 XSS 취약점이 있는 경우 CSRF 공격에 취약해진다.

domain 단위 검증에서 좀 더 세밀하게 페이지 단위까지 일치하는지 검증을 하면 도메인 내의 타 페이지에서의 XSS 취약점에 의한 CSRF 공격을 방어할 수 있다

 

 


 

 

 

 

 

아직 login은 구현하지 않았기 때문에

DB Workbench에서 확인해보니

 

🌟 짜잔 🌟

 

 

성공적으로 완료!!!!!

비밀번호는 “비밀이지롱” 이라고 입력했지만 passwordEncoder가 암호화하여 저장!

 

후후 근데 로그인으로 넘어가기 전에 유효성검사 먼저 추가해야한다

지금은 Null으로도 NullNull하게 가입가능;;;;;ㅜㅜ

 

 

다음편 @Valid 레츠고 💨