영화 예매 사이트 만들기 (6)
SpringBoot & React & h2
git -> https://github.com/leejinagood/MOVIE_Site.git
front -> https://github.com/leejinagood/MOVIE_Site_Front.git
build.gradle에 코드 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
SecurityConfig.java
package com.example.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(
new AntPathRequestMatcher("/**")).permitAll()
.and()
.csrf().ignoringRequestMatchers(
new AntPathRequestMatcher("/h2-console/**"))
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
;
return http.build();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
@Configuration은 스프링의 환경설정 파일임을 의미하는 애너테이션이다. 여기서는 스프링 시큐리티의 설정을 위해 사용되었다.
@EnableWebSecurity는 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션이다.
테이블 명을 SITE_MEMBER로 수정하였다.
갑자기 잘 실행되던 중
error starting applicationcontext. to display the conditions report re-run your application with 'debug' enabled.
error starting tomcat context. exception: org.springframework.beans.factory.beancreationexception.
라는 에러 두 개가 생겼다...
아아..
구성편집에
디버그 출력 활성화에 체크를 한 후
build.gradle에 시큐리티 두 줄을 주석처리한 후 실행하였더니 잘 된다.
회원가입과 로그인은 점프 투 스프링부트를 참고하였다.
패키지는 위의 형태와 같다.
MemberController.java
package com.example.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
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;
@RequiredArgsConstructor
@Controller
@RequestMapping("/member")
public class MemberController {
private final MemberService memberService;
@GetMapping("/signup")
public String signup() {
return "signup_form";
}
@GetMapping("/login")
public String login() {
return "login_form";
}
@PostMapping("/signup")
public String signup(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "signup_form";
}
return "redirect:/";
}
}
먼저 MemberController이다. 여기서는 로그인과 회원가입 둘 다 매핑을 지정해주었다.
귀찮아서 그런 건 절대 아니다.
MemberRepository.java
package com.example.MemberService;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<SiteMember, String> {
Optional<SiteMember> findByusername(String userid);
}
SiteMember의 기본키의 형태가 String이므로,,
MemberService.java
package com.example.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
public SiteMember create(String userid, String email, String password) {
SiteMember member = new SiteMember();
member.setUserid(userid);
member.setEmail(email);
member.setPassword(password);
this.memberRepository.save(member);
return member;
}
}
Member 리포지터리를 사용하여 Member 데이터를 생성하는 create 메서드를 추가했다.
SiteMember.java
package com.example.MemberService;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
public class SiteMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String userid;
@Column(unique = true)
private String password;
@Column(unique = true)
private String email;
}
엔티티명을 Member 대신 SiteMember로 한 이유는 스프링 시큐리티에 이미 Member 클래스가 있기 때문에 이렇게 설정하였다.
SecurityConfig.java
package com.example.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(
new AntPathRequestMatcher("/**")).permitAll()
.and()
.csrf().ignoringRequestMatchers(
new AntPathRequestMatcher("/h2-console/**"))
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
;
return http.build();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
and().formLogin().loginPage("/user/login").defaultSuccessUrl("/") 은 스프링 시큐리티의 로그인 설정을 담당하는 부분이다.
로그인 페이지의 URL은 /user/login이고 로그인 성공시에 이동하는 디폴트 페이지는 루트 URL(/)임을 의미한다.
UserRole.java
package com.example.UserService;
import lombok.Getter;
@Getter
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
UserRole(String value) {
this.value = value;
}
private String value;
}
UserSecurityService.java
package com.example.UserService;
import com.example.MemberService.MemberRepository;
import com.example.MemberService.SiteMember;
import lombok.RequiredArgsConstructor;
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 java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String userid) throws UsernameNotFoundException {
Optional<SiteMember> _siteMember = this.memberRepository.findByusername(userid);
if (_siteMember.isEmpty()) {
throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
}
SiteMember siteMember = _siteMember.get();
List<GrantedAuthority> authorities = new ArrayList<>();
if ("admin".equals(userid)) {
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
} else {
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
}
return new User(siteMember.getEmail(), siteMember.getPassword(), authorities);
}
}
스프링 시큐리티에 등록하여 사용할 UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현(implements)해야 한다. 스프링 시큐리티의 UserDetailsService는 loadUserByUsername 메서드를 구현하도록 강제하는 인터페이스이다. loadUserByUsername 메서드는 사용자명으로 비밀번호를 조회하여 리턴하는 메서드이다.
이제 내가 할당된 서비스는 거의 다 끝났다. 리액트로 화면 만들어지면 매핑 찍어주고 테스트만 해보면 될 것 같다!