Spring Boot - 10

현곤·2024년 12월 23일

UserController

/user/signup URL이 GET 요청이 되면 회원 가입을 위한 템플릿을 렌더링 후,

POST로 요청되면 회원 가입을 진행하도록 만듬

@Controller
@RequiredArgsConstructor
@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 "sigun_form";
        }

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

        userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(),
                userCreateForm.getPassword1());

        return "redirect:/";
    }
}

비밀번호 맞는지 확인

회원 가입 시 password1password2 가 동일한지를 검증하는 조건문,

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

2개의 값이 일치하지 않을 경우 bindingResult.rejectValue 사용하여,

입력 받은 2개의 비밀번호가 일치하지 않는다는 오류 발생

bindingResult.rejectValue("password2", "passwordInCorrect",
                    "2개의 패스워드가 일치하지 않습니다.");

bindingResult.rejectValue 의 매개변수의 순서

Value(필드명, 오류 코드, 오류 메시지)


회원 가입 데이터 저장

이후 userService.create 메서드를 사용해 사용자로부터 전달받은 데이터 저장

userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(),
                userCreateForm.getPassword1());

        return "redirect:/";

userCreateForm.get 을 사용해서 받은 아이디, 이메일, 비밀번호를 저장

userService.create 안에는 아이디와 이메일, 비밀번호를 받을 수 있게 set 을 사용

@Service
@RequiredArgsConstructor
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;
    }
}

회원 가입 중복된 아이디 오류내기

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:/";
    }

ID 또는 이메일 주소가 이미 존재할 경우

DataIntegrityViolationException

예외가 발생하니까 이미 등록된 사용자라는 오류 메시지를 표시

그 밖에 다른 예외들은 해당하는 예외에 대한 구체적인 오류 메시지를 보이게끔

e.getMessage() 를 사용

bindingResult.reject(오류 코드, 오류 메시지)

UserCreateForm 의 검증에 의한 오류 외에 일반적인 오류를 발생시킬 때 사용


로그인 기능 구현

로그인 메서드

.formLogin((formLogin) -> formLogin
                .loginPage("/user/login")
                .defaultSuccessUrl("/"))

.formlogin 메서드는 스프링 시큐리티의 로그인 설정을 담당하는 부분

설정 내용은 /user/login 로그인 성공 시에 이동할 페이지는 루트 URL (/)

로그인 매핑하기

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

/user/login 로 GET 요청을 이 메서드가 처리

그리고 매핑한 메서드는 login_form.html 템플릿을 출력하도록 만듬

실제 로그인을 진행하는 @PostMapping 은 스프링 시큐리티가 대신 처리함

그래서 만들 필요는 X

템플릿은 너무 어렵다 저거 어떻게 다 이해하지 프론트엔드 너무 어렵다

스프링 시큐리티 로그인 수행 방법

시큐리티 설정 파일에 사용자 ID, 비밀번호 직접 등록 인증 처리 메모리 방식을 사용

회원 가입을 통해 회원 정보를 DB에 저장했으므로 DB에 회원 정보를 조회해

로그인 하는 방법을 사용

  1. DB에서 사용자를 조회하는 서비스 만들기

  2. 서비스를 스프링 시큐리티에 등록하기

UserSecurityService 를 만들고 UserRepository 를 수정,

UserRole 클래스를 생성하는 등 준비를 해야함

findByusername 추가

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

스프링 시큐리티는 인증뿐만 아닌 권한도 관리함

사용자 인증 후 사용자에게 부여할 권환, 관련된 내용이 필요

그러면 사용자가 로그인한 후 ADMIN 또는 USER 같은 권한을 부여해야 함

UserRole 만들기

UserRoleenum 자료형 (열거 자료형)으로 작성

관리자를 의미하는 ADMIN 과 사용자를 의미하는 USER 라는 상수를 만듬

그리고 ADMIN 은 ROLE_ADMIN, USER 는 ROLE_USER 라는 값을 부여

UserRoleADMIN , USER 상수는 값을 변경할 필요가 없어서 @Getter 만 사용

ADMIN 권한을 가진 사용자는
다른 사람이 작성한 질문, 답변을 수정 가능하도록 만들 수 있음

UserSecurityService 생성

@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);
    }
}

스프링 시큐리티가 로그인시 사용할 UserSecurityService

스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현해야 한다.

구현 = implements << 그래서 사용한 거

스피링 시큐리티의 UserDetailsServiceloadUserByUsername 메서드를 구현하도록
강제하는 인터페이스

loadUserByUsername 메서드는 사용자명(username)으로
스프링 시큐리티의 사용자 객체를 조회하여 리턴

loadUserByUsername 메서드는 사용자명으로 Siteuser 객체를 조회

사용자명에 해당하는 데이터가 없을 경우 UsernameNotFoundException 발생

사용자명이 admin인 경우 ADMIN 권한을 부여, 그 외 USER 권한을 부여

마지막으로 User 객체를 생성해 반환

이 객체는 스프링 시큐리티에 사용하며
User 생성자엔 사용자명, 비밀번호, 권한 리스트가 전달

스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의

비밀번호가 사용자로부터 입력받은 비밀번호와 일치하는지를
검사하는 기능을 내부에 가지고 있다.

모르겠어서 적는 코드 설명

@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
  • @RequiredArgsConstructor
    • final이 붙은 필드 (userRepository)를 자동으로 주입
    • userRepository는 데이터베이스에서 사용자 정보를 가져오는 데 사용
@Override
public UserDetails loadUserByUsername(String username)
    throws UsernameNotFoundException {
  • Spring Security가 사용자를 검색할 때 호출하는 메서드

  • 파라미터 username : 로그인 시 입력된 사용자 이름

  • 반환값 : 사용자 정보를 담고 있는 userDetails 객체

Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);
if (_siteUser.isEmpty()) {
    throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
}
SiteUser siteUser = _siteUser.get();
  • findByusername (username) :
    • 데이터베이스에서 username 에 해당하는 사용자를 찾는 메서드
    • Optional 로 감싸서 변환 (값이 없을 경우 Optioanl.empty()
  • isEmpty() :
    • 사용자가 없으면 에러를 던져 로그인 실패 처리
List<GrantedAuthority> authorities = new ArrayList<>();
if ("admin".equals(username)) {
    authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
} else {
    authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
}
  • 사용자에게 부여된 권한을 설정하는 부분

  • admin 사용자에겐 ADMIN 권한 추가

  • 그 외 사용자에겐 USER 권한 추가

  • GrantedAuthority : Spring Security에서 권한을 표현하는 객체

return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
  • Spring Security의 User 객체를 생성해서 반환
    1. siteUser.getUsername() : 사용자의 이름
    2. siteUser.getPassword() : 사용자의 암호화된 비밀번호
    3. authorities : 사용자의 권한 목록

이 코드 전체 흐름

  1. 로그인하면, Spring Security가 이 클래스의 loadUserByUsername 메서드를 호출

  2. username 으로 데이터베이스를 검색해 사용자를 찾음

    • 사용자가 없으면 에러를 반환
  3. 사용자의 정보와 권한을 Spring Security의 User 객체로 변환

  4. 변환된 User 객체를 반환하여 Spring Security가 인증 과정을 계속 진행

이 코드는 로그인한 사용자가 유효한지 확인,
Spring Security가 인증에 사용할 사용자 정보와 권한을 제공
사용자를 찾지 못하면 "사용자를 찾을 수 없습니다." 라는 에러를 던지고 인증 중단

UserDetailsService 는 Spring Security의 핵심적인 인증 인터페이스

비밀번호는 반드시 암호화된 형태로 저장되어 있어야 하며,

Spring Security 설정에서 암호화된 비밀번호을 비교할 수 있도록 구성


Spring Security 설정 수정

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

AuthenticationManager 스프링 시큐리티의 인증을 처리

AuthenticationManager 사용자 인증 시 앞에서 작성한,

UserSecurityServicePasswordEncoder 내부적으로 사용하여
인증과 권한 부여 프로세스를 처리

항목 추가

속성 추가

@ManyToOne
    private SiteUser author;

author 속성에는 @ManytoOne 을 적용

왜?

사용자 한 명이 질문을 여러 개 작성할 수 있기 때문

그래서 Many to One 을 사용했나보다

컨트롤러, 서비스 업데이트

@PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id,
                               @Valid AnswerForm answerForm, 
                               BindingResult bindingResult,
                               Principal principal) {

로그인한 사용자의 정보를 알려면 스프링 시큐리티가 제공하는 Principal 객체 사용

createAnswer 메서드에 Principal 객체를 매개변수로

지정하는 작업까지만 해두고 답변 서비스 수정

Principal.getName() 을 호출하면 현재 로그인한 사용자의 사용자명(ID)를 알 수 있음

Principal 객체를 사용하면 이제 로그인한 사용자명을 알 수 있어

사용자명으로 SiteUser 객체를 조회할 수 있음

SiteUser 를 조회할 수 있는 getUser 메서드를 UserService 에 추가

public SiteUser getUser(String username) {
        Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
        if (siteUser.isPresent()) {
            return siteUser.get();
        }
        else {
            throw new DataNotFoundException("siteuser not found");
        }
    }

getUser 메서드는 userRepositoryfindByusername 메서드를 사용해

쉽게 만들 수 있음

사용자명에 해당하는 데이터가 없을 경우 DataNoFoundException 발생

답변 내용 저장할 때 글쓴이 데이터도 저장

public void create(Question question, String content, SiteUser author) {
        Answer answer = new Answer();
        answer.setContent(content);
        answer.setCreateDate(LocalDateTime.now());
        answer.setQuestion(question);
        answer.setAuthor(author);
        this.answerRepository.save(answer);
    }

create 메서드에 SiteUser 객체를 추가로 전달, 작성자도 함께 저장하도록 수정

createAnswer 메서드 완성

public class AnswerController {
    private final QuestionService questionService;
    private final AnswerService answerService;
    private final UserService userService;

    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id,
                               @Valid AnswerForm answerForm, 
                               BindingResult bindingResult,
                               Principal principal) {

        Question question = this.questionService.getQuestion(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());

        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }

        this.answerService.create(question, answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s",id);
    }
}

pricipal 객체를 통해 사용자명 얻고
사용자명을 통해 SiteUser 객체를 얻어 답변을 등록할 때 사용


UserService 를 통해 SiteUser 도 불러오기

pricipal은 로그인한 사용자의 아이디만 제공하기 때문에

사용자 객체 전체가 필요할 때가 많다.

그래서 UserService를 통해서 SiteUser를 불러온 것

UserServiceusername 기반으로 데이터베이스에서 사용자 정보를 가져오고,

이를 SiteUser 객체로 반환하기 때문이다.


createQuestion 메서드 완성

@PostMapping("/create")
    public String questionCreate(@Valid QuestionForm questionForm,
                                 BindingResult bindingResult,
                                 Principal principal) {
        if (bindingResult.hasErrors()) {
            return "question_form";
        }
        SiteUser siteUser = this.userService.getUser(principal.getName());
        this.questionService.create(questionForm.getSubject(), 
        							questionForm.getContent(), siteUser);
        return "question_list";
    }

@preAuthorize("isAuthenticated()") 사용하기

이 어노테이션이 붙은 메서드는 로그인한 경우에만 실행됨

즉, 이 어노테이션을 메서드에 붙이면 해등 메서드는 로그인한 사용자만 호출할 수 있다.

@PreAuthorize("isAuthenticated()") 가 적용된 메서드가

로그아웃 상태에서 호출되면 로그인 페이지로 강제 이동

로그인이 필요한 메서드(질문 등록과 관련된 메서드)들에 @PreAuthorize("isAuthenticated()") 어노테이션 적용

Answer 컨트롤러, Question 컨트롤러에 붙이기

  • QuestionCotroller
@PreAuthorize("isAuthenticated()")
    @GetMapping("/create")
    public String questionCreate(QuestionForm questionForm) {
        return "question_form";
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/create")
    public String questionCreate(@Valid QuestionForm questionForm,
                                 BindingResult bindingResult,
                                 Principal principal) {
        if (bindingResult.hasErrors()) {
            return "question_form";
        }
        SiteUser siteUser = this.userService.getUser(principal.getName());
        this.questionService.create(questionForm.getSubject(), 
        							questionForm.getContent(), siteUser);
        return "question_list";
    }
  • AnswerController
@PreAuthorize("isAuthenticated()")
    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id,
                               @Valid AnswerForm answerForm, 
                               BindingResult bindingResult,
                               Principal principal) {

        Question question = this.questionService.getQuestion(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());

        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }

        this.answerService.create(question, answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s",id);
    }
}

@EnableMethodSecurity 어노테이션의 prePostEnbled = true

QuestionControlle, AnswerController 에서 로그인 여부를 판별할 때 사용한

@PreAuthorize 어노테이션을 사용하기 위해 반드시 필요한 설정

profile
코딩하는 곤쪽이

0개의 댓글