스프링 부트 쇼핑몰 프로젝트 with JPA (5)

스프링 시큐리티를 이용한 회원가입 및 로그인 -2

📌 로그인/로그아웃 구현하기

UserDetailsService 인터페이스
👉🏻 데이터베이스에서 회원 정보 가져옴.
👉🏻 localUserByUsername() 메소드 존재. 회원 정보 조회하여 사용자의 정보와 권한을 갖는 UserDetails 인터페이스 반환

UserDetail 인터페이스
👉🏻 스프링 시큐리티에서 회원의 정보를 담기 위해 사용하는 인터페이스
👉🏻 직접 구현 OR 스프링 시큐리티에서 제공하는 User 클래스(UserDetails 인터페이스를 구현하고 있는 클래스) 사용

기존의 MemberService가 UserDetailsService를 구현하게 하자.

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
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 org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;

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

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

    //로그인 할 User의 email을 파라미터로 전달받음.
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email);

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

        //UserDetail을 구현하는 User 객체를 반환.
        //User 객체를 생성하기 위해 회원의 정보를 넘겨 줌.
        return User.builder()
                .username(member.getName())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
        }
}

👆🏻MemberService가 UserDetailsService를 구현함. loadUserByUsername메소드를 오버라이딩 하고, 로그인 할 User의 email을 파라미터로 전달받음. UserDetail을 구현하는 User 객체를 생성하고 반환함.

Securityconfig

package com.shop.config;


import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
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
public class SecutiryConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MemberService memberService;

    //http 요청에 대한 보안 설정 (페이지 권한, 로그인 페이지, 로그아웃 메서드 설정 작성)
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.formLogin()
                .loginPage("/members/login")
                .defaultSuccessUrl("/")
                .usernameParameter("email")
                .failureUrl("/members/login/error")
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
                .logoutSuccessUrl("/");
    }

    //BCryptPasswordEncoder() 해쉬함수 이용해서 비밀번호를 암호화
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //spring Security에서 인증은 AuthenticationManagerBuilder 통해 이뤄짐
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(memberService)  //userDetail을 구현하는 객체로 memberService 지정
                .passwordEncoder(passwordEncoder());    //비밀번호 암호화
    }
}

👆🏻configure 메소드에서 로그인 페이지 URL을 설정, 로그인 성공 시 이동할 URL 설정, 로그인 시 사용할 파라미터 이름으로 email을 지정, 로그인 실패시 이동할 URL 설정, 로그아웃 URL을 설정함.
👆🏻스프링 시큐리티에서 인증은 AuthenticationManagerBuilder가 생성한 AuthenticationManeger을 통해 함.

Controller

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

....코드 생략.....

   @GetMapping("/login")
    public String loginMember(){
        return "member/memberLoginForm";
    }
    @GetMapping("/login/error")
    public String loginError(Model model){
        model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해주세요");
        return "/member/memberLoginForm";
    }
}

📌 실행화면

<로그인 화면>
<로그인 실패>

<로그인 성공>

📌 페이지 권한 설정하기

👉🏻 상품 등록 페이지는 ADMIN 계정만 가능하게 설정함.

ItemController

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {
    @GetMapping("/admin/item/new")
    public String itemForm(){
        return "/item/itemForm";
    }
}

👆🏻상품 등록 페이지로 접근할 수 있도록 Controller에 설정함.

package com.shop.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

👆🏻AuthenticationEntryPoint 인터페이스를 통해 인증되지 않은 사용자가 리소스를 요청할 경우 에러가 발생하도록 함.

SecutiryConfig

@Override
    protected void configure(HttpSecurity http) throws Exception{
    
    .....코드 생략....
    
    http.authorizeRequests()
                .mvcMatchers("/", "/members/**",
                        "/item/**", "/images/**").permitAll()
                .mvcMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated();

        http.exceptionHandling()
                .authenticationEntryPoint(
                        new CustomAuthenticationEntryPoint());
    }
    
    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
    }

좋은 웹페이지 즐겨찾기