Spring/초기 setting

6) 스프링 시큐리티(spring-security) 적용

쿠카이든 2023. 4. 5. 15:51
728x90
  • 스프링 시큐리티 적용 방법
    • 이제 build.gradle Spring Security 의존성을 추가해준다.
    • 프로젝트를 실행하니 처음 보는 security password가 콘솔에 찍히게 된다.
implementation 'org.springframework.boot:spring-boot-starter-security'

패스워드가 생성됨

  • 임의로 localhost:8080/hello에 접근을 해보았다.
  • 302 상태가 반환되며 localhost:8080/login으로 리다이렉트가 되었다.

login 페이지로 리다이렉트

  • 위 로그인 화면은 Spring Security가 기본으로 제공해주는 화면이다. 
  • Username에 user, Password에 콘솔에 찍힌 문자열을 입력하면 정상적으로 localhost:8080/hello로 넘어가게 된다.
    • 참고로 localhost:8080/logout을 요청하면 로그아웃 페이지가 나온다.

 

그리고 application.properties에 username과 password를 지정할 수 있다.

spring.security.user.name=user
spring.security.user.password=(비밀번호)

이렇듯 Spring Security 의존성만을 추가했음에도 불구하고 기본적인 로그인 기능이 제공되었다.

 

  • 참고로 Spring Security이 기본적으로 제공하는 로그인은 Session을 이용한 로그인이다.
    • 로그인한 브라우저의 요청 헤더 JSESSIONID값을 복사해 로그인하지 않은 브라우저에 JSESSIONID값을 넣어 주면 로그인이 된 상태가 된다. 반대로 로그인이 된 브라우저에서 JSESSIONID값을 지우면 로그인이 풀리게 된다.

 

실제 설정 구현 방법 예제

package com.example.settingtest.config;

import com.example.settingtest.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final MemberService memberService;
    
    //http 요청에 대한 보안 설정
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginPage("/members/login") //로그인 페이지 URL
            .defaultSuccessUrl("/")      //로그인 성공 시 이동할 URL
            .usernameParameter("email")   //로그인 시 사용할 파라미터 네임을 email로 설정
            .failureUrl("/members/login/error")   //로그인 실패 시 이동할 URL 설정
            .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) //로그아웃 URL 설정
            .logoutSuccessUrl("/")    //로그아웃 성공 시 이동할 URL 설정
        ;

        http
            .authorizeRequests()   //시큐리티 처리에 httpServletRequest를 이용함을 의미
            .mvcMatchers("/", "/members/**", "/item/**", "/api/v1/app/**").permitAll()
            .mvcMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .csrf().disable()
        ;
        
        //인증되지 않은 사용자가 리소스에 접근하였을 떄 수행되는 핸들러를 등록
        http.exceptionHandling()
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        ;
    }
    
    //BCryptPasswordEncoder의 해시 함수를 이용하여 비밀번호를 암호화하여 저장
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberService)
            .passwordEncoder(passwordEncoder());
    }
    
    //static 디렉터리의 하위 파일은 인증을 무시하도록 설정(이미지, css 등)
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
    }

}
  • @EnableWebSecurity - WebSecurityConfigurerAdapter를 상속받는 클래스에 @EnableWebSecurity 어노테이션을 선언하면 SpringSecurityFilterChain이 자동으로 포함된다. WebSecurityConfigurerAdapter를 상속받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징 할 수 있다.
  • .mvcMatchers 관련
    • permitAll() - 해당 경로에 모든 사용자가 인증없이 접근할 수 있도록 설정함
    • hasRole(“ADMIN”) - 해당 계정이 ADMIN role 일 경우에만 접근이 가능하도록 설정
    • anyRequest().authenticated() - 위에 경로들을 제외한 나머지 경로들은 모두 인증을 요구하도록 설정
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(memberService)
        .passwordEncoder(passwordEncoder());
}
  • Spring Security에서 인증은 AuthenticationManager를 통해 이루어지며 AuthenticationManagerBuilder가 AuthenticationManager를 생성한다. userDetailService를 구현하고 있는 객체로 memberService를 지정해주며, 비밀번호 암호화를 위해 passwordEncoder를 지정해준다.

 

(loadUserByUsername 구현)

@RequiredArgsConstructor
@Service
public class MemberService implements UserDetailsService {

    private final MemberJpaRepository memberJpaRepository;

    ...

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        Member member = memberJpaRepository.findByEmail(email);

        if(member == null){
            throw new UsernameNotFoundException(email);
        }

        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }
}
  • MemberService가 UserDetailsService를 구현한다.
  • UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩합니다. 로그인할 유저의 email을 파라미터로 전달받는다.
  • UserDetail을 구현하고 있는 User 객체를 반환해줍니다. User 객체를 생성하기 위해서 생성자로 회원의 이메일, 비밀번호, role을 파라미터로 넘긴다.
728x90
  • 회원 가입 test
@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Member createMember(){
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail("test@email.com");
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("서울시 마포구 합정동");
        memberFormDto.setPassword("1234");
        return Member.createMember(memberFormDto, passwordEncoder);
    }

    @Test
    @DisplayName("회원가입 테스트")
    public void saveMemberTest(){
        Member member = createMember();
        Member savedMember = memberService.join(member);
        assertEquals(member.getEmail(), savedMember.getEmail());
        assertEquals(member.getName(), savedMember.getName());
        assertEquals(member.getAddress(), savedMember.getAddress());
        assertEquals(member.getPassword(), savedMember.getPassword());
        assertEquals(member.getRole(), savedMember.getRole());
    }

    @Test
    @DisplayName("중복 회원 가입 테스트")
    public void saveDuplicateMemberTest(){
        Member member1 = createMember();
        Member member2 = createMember();
        memberService.join(member1);
        Throwable e = assertThrows(IllegalStateException.class, () -> {
            memberService.join(member2);});
        assertEquals("이미 가입된 회원입니다.", e.getMessage());
    }
}

 

  • 회원 가입 단위테스트 성공화면

  • 로그인 test
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class MemberControllerTest {

    @Autowired
    private MemberService memberService;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Member createMember(String email, String password){
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail(email);
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("서울시 마포구 합정동");
        memberFormDto.setPassword(password);
        Member member = Member.createMember(memberFormDto, passwordEncoder);
        return memberService.join(member);
    }

    @Test
    @DisplayName("로그인 성공 테스트")
    public void loginSuccessTest() throws Exception{
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password);
        mockMvc.perform(formLogin().userParameter("email")
                .loginProcessingUrl("/members/login")
                .user(email).password(password))
                .andExpect(SecurityMockMvcResultMatchers.authenticated());
    }

    @Test
    @DisplayName("로그인 실패 테스트")
    public void loginFailTest() throws Exception{
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password);
        mockMvc.perform(formLogin().userParameter("email")
                .loginProcessingUrl("/members/login")
                .user(email).password("12345"))
                .andExpect(SecurityMockMvcResultMatchers.unauthenticated());
    }
}

 

  • 로그인 단위 테스트 성공 화면

 

참고 : 스프링 부트 쇼핑몰 프로젝트 with JPA - 변구훈 저

728x90

'Spring > 초기 setting' 카테고리의 다른 글

8) API Response(@ControllerAdvice) 설정  (0) 2023.04.20
mapStruct 소개 및 활용  (0) 2023.04.10
5) 스프링 시큐리티(spring-security) 개요  (0) 2023.04.05
4) Querydsl 설정  (2) 2023.03.20
3) JPA 다중 스키마 설정  (0) 2023.03.19