영속성 컨텍스트의 개념은 JPA에서 매우 중요한 개념.

 

일종의 "캐시"라고 생각할 수 있음.

 

1. 엔티티의 상태를 관리하는 메모리 공간.

2. JPA에서 엔티티 매니저가 영속성 컨텍스트를 관리함.

3. DB하고 상호작용하기 전에 엔티티 객체의 상태를 유지하고 추적

 

1차 캐시 (First-Level Cache)

 - 1차 캐시역할을 해서 db에서 조회하고 영속성 컨텍스트에 저장됨.

 그리고 동일한 엔티티를 조회하면 db안타고 영속성 컨텍스트에 가져옴.(캐시에서 가져온다 생각)

 

엔티티 생명주기

비영속 (Transient) : 엔티티가 새로 생성되었지만 영속성 컨텍스트에 저장 X

영속 (Persistent) : 엔티티가 영속성 컨텍스트에 저장

준영속 (Detacghed) : 영속성 컨텍스트에서 분리된 상태

삭제 (Removed) : 엔티티가 삭제된 상태

 

변경 감지( Dirty Checking )

 - 영속성 컨텍스트는 엔티티의 변경을 감지 , 트랜잭션이 커밋될 때 변경된 내용을 db에 저장

 

지연 로딩 ( Lay Loading )

 - 연관된 엔티티를 필요할 때까지 로딩하지 않음.

 

em.flush()를 이용해서 강제로 db에 반영하는 것임.

그리고 em.clear()해서 테스트 하는이유가 캐시에서 엔티티를 제거하고 , 다음 호출할때 새로운 데이터를 가져오게 되는데

그게 join이 잘되었는지 확인하기 위해서 테스트 할 때 사용하는 것.

 

    @Test
    @DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")
    public void findCartAndMemberTest(){
        Member member = createMember();
        memberRepository.save(member);

        Cart cart = new Cart(); // 카트 생성
        cart.setMember(member); // 장바구니 주인 담음
        cartRepository.save(cart); // 기능이용할려고 save 할려고 cart를

        em.flush(); // 트랜잭션 끝날때 db반영 매니저로부터 강제 호출해서 저장하는 것임
        em.clear(); // 실제 db에서 장바구니 엔티티를 가져 올 때 회원 엔티티도 같이 가져오는지 위해서 비워줌.

        Cart savedCart = cartRepository.findById(cart.getId()) // 카트 생성해서 저장된 아이디 가져옴
                .orElseThrow(EntityNotFoundException::new); //cart.getId 없으면 해당 익셉션 날리는 것
        assertEquals(savedCart.getMember().getId(), member.getId());

    }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

계정 권한에 따라 상태 노출하는 라이브러리 테스트 및 사용

 

인증된 사용자 정보를 가져오거나 , 특정 권한 있는지 확인 가능

 

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>{version}</version> <!-- 사용하려는 라이브러리 버전으로 변경해야 합니다 -->
</dependency>

 

원하는 버전 선택 후 추가

 

그리고 html에서는 다음과 같이 사용

 

Dependency 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:' not found 오류

 

스프링 부트 3.XX 이상 부터는 6버전 사용해야 함

 

html에 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

sec 추가해야지 사용할 수 있음.

 

- 인증된 사용자의 정보 출력
- 사용자의 권한에 따라 페이지의 일부를 보여주거나 가리기
- 로그인 폼에서 사용자 이름과 비밀번호 필드를 렌더링
- 로그인 에러 메시지 출력 등

 

따라서 xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 선언은 Thymeleaf 템플릿에서 Spring Security와 관련된 기능을 사용하기 위한 중요한 부분입니다. 

 

이를 통해 Spring Security와 Thymeleaf를 통합하여 보안 기능을 쉽게 사용할 수 있습니다.

 

<li class="nav-item" sec:autorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/admin/item/new">상품 등록</a>
                    </li>

 

sec문법 이용해서 해당 권한을 가진 사람만 보여줄 수 있게 가능하게 함

 

왜 안되나 살펴봤는데 스펠링 틀렸음

sec:authorize

 

문법 틀리면 전부 보여주기 때문에 조심해야 함

.logout(logoutConfig -> logoutConfig
                        .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
                        .logoutSuccessUrl("/")

기존 "members/logout" 으로 되어있었는데 -> "/members/logout" 으로 바꿔줘야지 정상 동작

MockMvc를 이용해서 테스트에 필요한 기능만 가지는 가짜 객체를 생성해서 테스트 하려고 했음.

 

근데 formLogin() 기능이 여기서도 먹히지 않음

 

security.test에서 사용하는데 현재 버전에서는 사용 안하는건가?

 

 

test 오류나서 인식 못함 추가했는데 왜 안될까..?

 

org.springframework.boot:spring-boot-starter-test:jar:6.3.0-SNAPSHOT was not found in https://repo.spring.io/snapshot during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of spring-snapshots has elapsed or updates are forced

 

org.springframework.boot:spring-boot-starter-test:jar:6.3.0-SNAPSHOT은 이전 시도 중에 https://repo.spring.io/snapshot에서 찾을 수 없습니다. 이 실패는 로컬 저장소에 캐시되었으며 스프링 스냅샷의 업데이트 간격이 경과되거나 업데이트가 강제될 때까지 해결이 다시 시도되지 않습니다.

 

그냥 -test로 추가해놓고 왜 안되는지 했음.. security-test 추가하니 잘 됨.

근데 formLogin은 현재 안되고 있어서 다른 방법으로 해결해야 함.

 


    @Test
    @DisplayName("Mock 가짜 로그인 성공 테스트")
    public void MockLoginTest() throws Exception{
        String email = "test@naver.com";
        String password = "1234";
        this.createMember(email, password);
        mockMvc.perform(MockMvcRequestBuilders.post("/members/login")
                .param("email" , email)
                .param("password", password)
                .andExpect(SecurityMockMvcResultMatchers.authenticated());
                -- andExpect 사용 안됨
    }

 

.andExpect 함수가 안돼서 같은 방법으로 테스트 불가능 왜 안될까?

 

andExpect는 ResultActions 객체의 메서드 중 하나임 요청에 대한 결과 검증하는데 사용

 

전에는 formLogin안에서 둬서 잘되었지만 현재는 그렇게 하면 안됨

 

 

    @Test
    @DisplayName("Mock 가짜 로그인 성공 테스트")
    public void MockLoginTest() throws Exception{
        String email = "test@naver.com";
        String password = "1234";
        this.createMember(email, password);
        ResultActions resultActions =  mockMvc.perform(MockMvcRequestBuilders.post("/members/login")
                .param("email" , email)
                .param("password", password));

        resultActions.andExpect(SecurityMockMvcResultMatchers.authenticated());
    }

 

이렇게하면 로긍니 성공 인증이 된 걸 테스트 할 수 있음.

    @Test
    @DisplayName("Mock 가짜 로그인 실패 성공 테스트")
    public void MockLoginFaiTest() throws Exception{
        String email = "test@naver.com";
        String password = "1234";
        this.createMember(email, password);
        ResultActions resultActions =  mockMvc.perform(MockMvcRequestBuilders.post("/members/login")
                .param("email" , email)
                .param("password", "12345"));

        resultActions.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
    }

 

12345로 틀리게 비밀번호 입력했을 때 검증했을 때 잘되는 것 확인

시큐리티에서 formLogin 등등 많은 코드들 사용하지 말라고한다.

람다를 사용하라고 함!

 

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable);
        http
                .authorizeHttpRequests(
                        authorize -> authorize
                                .requestMatchers("/resources/**").permitAll()
                                .requestMatchers("/**").permitAll()
                                .requestMatchers("/admin/**").hasRole("ADMIN")
                                .requestMatchers("/members/**").permitAll()
                                .anyRequest().denyAll()
                )
                .formLogin(formLogin -> formLogin
                        .loginPage("/members/login")
                        .defaultSuccessUrl("/")
                        .usernameParameter("email")
                        .failureUrl("/members/login/error")
                )
                .logout(logoutConfig -> logoutConfig
                        .logoutRequestMatcher(new AntPathRequestMatcher("members/logout"))
                        .logoutSuccessUrl("/")
                )
        ;

        return http.build();
    }

 

해결 완료!

스프링 시큐리티에서 현재 사용하지 않는 문법 , deprecated된 메서드들이 많음.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable);
        http
                .authorizeHttpRequests(
                        authorize -> authorize
                                .requestMatchers("/member/join").permitAll()
                                .requestMatchers("/auth/login").permitAll()
                                .anyRequest().authenticated()
                );
        return http.build();
    }

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

 

@Configuration 어노테이션은 클래스가 Spring의 설정 클래스 , 애플리케이션의 구성을 정의

@EnableWebSecurity는 웹 보안 활성

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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

전부 security문법에서 사용되는 것

 

filterchain을 제정의함으로써 어떤식으로 활성 , 비활성할 것 인지 정함. 

requestMatchers는 해당 접근만 허용하고 다른 요청은 인증 필요하게 설정한다.

 

 


다른곳에서 에러나는데

 

An error happened during template parsing 에러남

 

로그를 살펴보니 로그인 하지 않았을때 csrf 토근이 null이여서 에러터짐 null이 아닐 경우에만 받게 수정

 

 

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

<!-- CSRF 토큰이 null이 아닐 때만 출력 -->
<input type="hidden" th:if="${_csrf != null}" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

        <div class="form-group">
            <label th:for="address">주소</label>
            <input type="text" th:field="*{address}" class="form-control" placeholder="주소 입력">
            <p th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="fieldError">Incorrect data</p>
        </div>

 

* 애스터리스크는 타임리프 바인딩 기능

 

name값이 자동으로 address로 설정 됨 그래서 name속성 따로 없음.

        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e){
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }

 

객체의 상태가 메서드 호출했을때 나는 IllegalStateException에 필드값이 입력되어잇지 않으면

valid가 체크한뒤  

@NotEmpty(message = "이메일은 필수 입력 값.")

 

해당 메시지를 담아서 보냄 그럼 바인딩 되어 있는 th:errors 해당 필드오류를 보냄

1. 시큐리티 사용하기 위해서 일단 의존성 등록

메이븐 가서 찾아봄.

 

넣어줍니다.

 

꼭 해보고 메이븐 업데이트 한 뒤 오류나는지 안나는지 확인 오류날경우 의존성 충돌 또는 버전 안맞아서 생기는 문제

 

의존성만 추가해도 스프링에서 제공하는 로그인 페이지로 이동함 이걸 커스텀해서 사용 

 

기본 제공하는 아이디 user

비밀번호는 콘솔창에 나옴

복사해서 로그인 하면 됨

 

로그인이 필요한 화면 , 필요없는 화면 생각해서 생성 

 

SecurityConfig 소스 작성

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>3.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
package com.shop.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .mvcMatchers("/", "/public/**").permitAll() // mvcMatchers를 사용하여 URL 패턴을 지정
                                .anyRequest().authenticated()

                .formLogin(withDefaults());
        return http.build();
    }

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

 

SecurityFilterChain으로 HTTP 요청을 처리하고 보안을 적용하는 것을 재처리함

autohrizeHttpRequests는 요청에 대한 접근을 설정하고 permitAll은 모든 사용자에게 해당 접근을 허용

 

그리고 passwordEncdoer는 패스워드 암호화 어떻게 하는지에 대한 메소드.

 

하지만 위의 코드에서 mvcMatchers가 정상동작하지 않고 있음

 

ExpressionIntercepUrlRegistry 클래스에서 antMatchers() 메소드를 제공하는데

이것은 spring-security-config 라이브러리에 포함되어 있어서 라이브러리 등록이 안되어 있으면 사용을 못한다는 에러

 

antMatchers랑 mvcMatachers 둘 다 사용이 X requestMatchers 사용

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

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/", "/public/**").permitAll() // mvcMatchers를 사용하여 URL 패턴을 지정
                                .anyRequest().authenticated()
                )
                .formLogin(withDefaults());
        return http.build();
    }

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

 

역할 구분 ROLE상수 만들어주고 ,

회원가입 담아줄 DTO 생성 , 

회원정보 저장해줄 엔티티 생성 , 

Member엔티티를 db에 저장할 수 있도록 Repository까지 생성

 

package com.shop.service;


import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public Member saveMember(Member member){
        validateDuplicateMember(member);
        return memberRepository.save(member);
    }

    public void validateDuplicateMember(Member member){
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if(findMember != null){
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }

}

Service를 분석하면 

@Transactional 트랜잭션 내에서 실행되어야 함. 모든 작업 하나로 묶임.

@RequiredArgsConstructor 어노테이션은 final로 선언된 필드를 가지고 생성자 자동 생성

new 계속 해준다는 얘기 없으면 new로 계속 객체 생성한다음 사용해야 함

 

객체의 부적절한 상태를 처리할때 IllegalStateException을 처리해서 사용하기 때문에 이렇게 처리

다른식으로 하는데 코드의 의미 부여

 

 

테스트 코드 작성 오른쪽 누르고 Go To 해서 만들 수 있음

 

테스트가 통과를 못하고 있는데 member쪽에 문제가 있는듯 ..

 

인메모리 방식인 h2에서 문제가 생긴 것 같음

 

설정도 application-test.properties사용하고 있음

 

이것저것 찾아보았음

spring.jpa.hibernate.ddl-auto=create-drop

이 설정으로 다 지우고 다시 만드는데도 잘 안됨

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>

버전을 1.4.199로 다운그레이드하니깐 잘 됨.

 

원인도 궁금해서 찾아봔슨데 H2 1.4이상 버전부터는 보안 상 문제로 스프링부트 가동 시 자동 생성해도 안된다고 함 그래서 DB를 찾지 못했음.

<scope>runtime</scope>해도 소용없음

 

안정화된 옛날 버전 쓰자..


    @Test
    @DisplayName("중복 테스트")
    public void saveDuplicateTest(){
        Member member1 = new Member();
        Member member2 = new Member();
        memberService.saveMember(member1);

        // 예외 던짐
        Throwable e = assertThrows(IllegalStateException.class, () -> {
           memberService.saveMember(member2);
        });

        assertEquals("이미 가입된 회원입니다." , e.getMessage());
    }

 

테스트 멤버 2개 생성하고 , 서비스 테스트 할꺼니깐 saveMember서비스로 저장시켜줌 member1

 

그 다음 member2를 저장시킬때 assertThrows로 예외를 테스트하는 메서드로 예외를 던지는 것을 예상

 

그리고 e로 예외 받은것을 예외 문구하고 비교했을 때 정상작동하는지 본 것

 

+ Recent posts