/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:/";
}
}
회원 가입 시 password1 이 password2 가 동일한지를 검증하는 조건문,
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에 회원 정보를 조회해
로그인 하는 방법을 사용
DB에서 사용자를 조회하는 서비스 만들기
서비스를 스프링 시큐리티에 등록하기
UserSecurityService 를 만들고 UserRepository 를 수정,
UserRole 클래스를 생성하는 등 준비를 해야함
public interface UserRepository extends JpaRepository<SiteUser, Long> {
Optional<SiteUser> findByusername(String username);
}
스프링 시큐리티는 인증뿐만 아닌 권한도 관리함
사용자 인증 후 사용자에게 부여할 권환, 관련된 내용이 필요
그러면 사용자가 로그인한 후 ADMIN 또는 USER 같은 권한을 부여해야 함
UserRole 은 enum 자료형 (열거 자료형)으로 작성
관리자를 의미하는 ADMIN 과 사용자를 의미하는 USER 라는 상수를 만듬
그리고 ADMIN 은 ROLE_ADMIN, USER 는 ROLE_USER 라는 값을 부여
UserRole 의 ADMIN , USER 상수는 값을 변경할 필요가 없어서 @Getter 만 사용
ADMIN 권한을 가진 사용자는
다른 사람이 작성한 질문, 답변을 수정 가능하도록 만들 수 있음
@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 << 그래서 사용한 거
스피링 시큐리티의 UserDetailsService 는 loadUserByUsername 메서드를 구현하도록
강제하는 인터페이스
loadUserByUsername 메서드는 사용자명(username)으로
스프링 시큐리티의 사용자 객체를 조회하여 리턴
loadUserByUsername 메서드는 사용자명으로 Siteuser 객체를 조회
사용자명에 해당하는 데이터가 없을 경우 UsernameNotFoundException 발생
사용자명이 admin인 경우 ADMIN 권한을 부여, 그 외 USER 권한을 부여
마지막으로 User 객체를 생성해 반환
이 객체는 스프링 시큐리티에 사용하며
User 생성자엔 사용자명, 비밀번호, 권한 리스트가 전달
스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의
비밀번호가 사용자로부터 입력받은 비밀번호와 일치하는지를
검사하는 기능을 내부에 가지고 있다.
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
@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);
User 객체를 생성해서 반환siteUser.getUsername() : 사용자의 이름siteUser.getPassword() : 사용자의 암호화된 비밀번호authorities : 사용자의 권한 목록로그인하면, Spring Security가 이 클래스의 loadUserByUsername 메서드를 호출
username 으로 데이터베이스를 검색해 사용자를 찾음
사용자의 정보와 권한을 Spring Security의 User 객체로 변환
변환된 User 객체를 반환하여 Spring Security가 인증 과정을 계속 진행
이 코드는 로그인한 사용자가 유효한지 확인,
Spring Security가 인증에 사용할 사용자 정보와 권한을 제공
사용자를 찾지 못하면 "사용자를 찾을 수 없습니다." 라는 에러를 던지고 인증 중단
UserDetailsService 는 Spring Security의 핵심적인 인증 인터페이스
비밀번호는 반드시 암호화된 형태로 저장되어 있어야 하며,
Spring Security 설정에서 암호화된 비밀번호을 비교할 수 있도록 구성
@Bean
AuthenticationManager authenticationManager (AuthenticationConfiguration
authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
AuthenticationManager 스프링 시큐리티의 인증을 처리
AuthenticationManager 사용자 인증 시 앞에서 작성한,
UserSecurityService 와 PasswordEncoder 내부적으로 사용하여
인증과 권한 부여 프로세스를 처리
@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 메서드는 userRepository 의 findByusername 메서드를 사용해
쉽게 만들 수 있음
사용자명에 해당하는 데이터가 없을 경우 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 객체를 추가로 전달, 작성자도 함께 저장하도록 수정
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를 불러온 것
UserService는 username 기반으로 데이터베이스에서 사용자 정보를 가져오고,
이를 SiteUser 객체로 반환하기 때문이다.
@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()") 어노테이션 적용
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 어노테이션을 사용하기 위해 반드시 필요한 설정