WEB 커리큘럼 5주차

이은지·2023년 11월 14일
0

WEB 커리큘럼 6주차에는 스프링 시큐리티를 이용하여 회원가입, 로그인과 로그아웃 기능을 구현하였다.

회원가입 기능 구현하기

먼저, 회원 정보를 위한 엔티티를 생성한다.

  • SiteUser 엔티티
    • unique = true: 유일한 값만 저장할 수 있음을 의미
@Getter
@Setter
@Entity
public class SiteUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true) 
    private String username;

    private String password;

    @Column(unique = true)
    private String email;
}

다음으로, User 리포지터리와 서비스를 구현한다.

  • UserRepository User 리포지터리
    • SiteUser의 PK타입은 Long이므로, JpaRepository<SiteUser, Long>처럼 사용
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<SiteUser, Long> {
}
  • UserService User 서비스
    • User 데이터를 생성하는 create 메서드 추가
    • 사용자 비밀번호 암호화를 위해 시큐리티의 BCryptPasswordEncoder 클래스를 사용하여 암호화하여 비밀번호를 저장
      • 이때, BCryptPasswordEncoder 객체를 직접 new로 생성하지 않고, 빈(bean)으로 등록

      • SecurityConfig.java

        (... 생략 ...)
        import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
        import org.springframework.security.crypto.password.PasswordEncoder;
        (... 생략 ...)
        
        @Configuration
        @EnableWebSecurity
        public class SecurityConfig {
           (...생략...)
        
            @Bean
            PasswordEncoder passwordEncoder() {
                return new BCryptPasswordEncoder();
            }
        }
    • 빈으로 등록한 Password 객체를 주입받아 서비스에서 사용
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
		private final PasswordEncoder passwordEncoder;

    public SiteUser create(String username, String email, String password) {
        SiteUser user = new SiteUser();
        user.setUsername(username);
        user.setEmail(email);
				user.setPassword(passwordEncoder.encode(password));
        this.userRepository.save(user);
        return user;
    }
}

이제, 회원가입 컨트롤러를 작성한다.

  • /user/signup URL이 GET으로 요청되면 회원 가입을 위한 템플릿을 렌더링
  • /user/signup URL이 POST로 요청되면 회원가입을 진행
  • 회원가입 시 발생하는 오류처리
    • 사용자 ID 또는 이메일 주소가 동일할 경우 DataIntegrityViolationException이 발생
    • bindingResult.reject(오류코드, 오류메시지)는 특정 필드의 오류가 아닌 일반적인 오류를 등록할때 사용
@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping("/signup")
    public String signup(UserCreateForm userCreateForm) {
        return "signup_form";
    }

    @PostMapping("/signup")
    public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "signup_form";
        }

        if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
            bindingResult.rejectValue("password2", "passwordInCorrect", 
                    "2개의 패스워드가 일치하지 않습니다.");
            return "signup_form";
        }

        try {
            userService.create(userCreateForm.getUsername(), 
                    userCreateForm.getEmail(), userCreateForm.getPassword1());
        }catch(DataIntegrityViolationException e) {
            e.printStackTrace();
            bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
            return "signup_form";
        }catch(Exception e) {
            e.printStackTrace();
            bindingResult.reject("signupFailed", e.getMessage());
            return "signup_form";
        }

        return "redirect:/";
    }
}

로그인, 로그아웃 기능 구현하기

로그인 URL 등록

먼저, 스프링 시큐리티에 로그인 URL을 등록하였다.

SecurityConfig.java

(... 생략 ...)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
            .headers((headers) -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
            .formLogin((formLogin) -> formLogin //로그인 URL 등록 부분 
                .loginPage("/user/login")
                .defaultSuccessUrl("/"))
        ;
        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 추가한 .formLogin 메서드는 스프링 시큐리티의 로그인 설정을 담당하는 부분으로 로그인 페이지의 URL은 /user/login이고 로그인 성공시에 이동하는 디폴트 페이지는 루트 URL(/)임을 의미

UserController에 로그인 매핑 추가

  • login_form.html 템플릿을 렌더링하는 GET 방식의 login 메서드를 추가
  • 실제 로그인을 진행하는 @PostMapping 방식의 메서드는 스프링 시큐리티가 대신 처리하므로 직접 구현할 필요가 없음
(... 생략 ...)
public class UserController {

    (... 생략 ...)

    @GetMapping("/login")
    public String login() {
        return "login_form";
    }
}

UserRepository에 findByusername 메서드 추가

  • 사용자 조회를 위함
import java.util.Optional;

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

public interface UserRepository extends JpaRepository<SiteUser, Long> {
    Optional<SiteUser> findByusername(String username);
}

UserRole 작성

  • 스프링 시큐리티는 인증 뿐만 아니라 권한도 관리, 따라서 인증후에 사용자에게 부여할 권한이 필요
  • 다음과 같이 ADMIN, USER 2개의 권한을 갖는 UserRole을 신규로 작성
import lombok.Getter;

@Getter
public enum UserRole {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER");

    UserRole(String value) {
        this.value = value;
    }

    private String value;
}

스프링 시큐리티 설정에 등록할 **UserSecurityService 작성**

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);
        if (_siteUser.isEmpty()) {
            throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
        }
        SiteUser siteUser = _siteUser.get();
        List<GrantedAuthority> authorities = new ArrayList<>();
        if ("admin".equals(username)) {
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        } else {
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}

스프링 시큐리티에 AuthenticationManager 빈을 생성

  • AuthenticationManager 빈을 생성
  • AuthenticationManager는 스프링 시큐리티의 인증을 담당
  • AuthenticationManager는 사용자 인증시 앞에서 작성한 UserSecurityService와 PasswordEncoder를 사용
(... 생략 ...)
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
(... 생략 ...)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    (... 생략 ...)

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

스프링 시큐리티로 로그아웃 구현

  • 다음과 같이 SecurityConfig파일을 수정
  • 로그아웃 URL을 /user/logout 으로 설정하고 로그아웃이 성공하면 루트 / 페이지로 이동
  • 로그아웃시 생성된 사용자 세션도 삭제하도록 처리
(... 생략 ...)

@Configuration
@EnableWebSecurity
public class SecurityConfig  {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
            .headers((headers) -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
            .formLogin((formLogin) -> formLogin
                .loginPage("/user/login")
                .defaultSuccessUrl("/"))
            .logout((logout) -> logout //로그아웃 부분
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true))
        ;
        return http.build();
    }

    (... 생략 ...)
}
profile
소통하는 개발자가 꿈입니다!

0개의 댓글