1. Spring Security
인증
,인가
를 위해 사용인가
는 인증 과정 이후, 관리자 페이지에 일반 유저 접근하지 못하는 것 등이 포함
2. Security 설정
https://hhseong.tistory.com/173
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
WebSecurityConfigurerAdapter
: 오버라이딩해서 보안 설정을 커스터마이징
3. 회원 가입
- 회원 정보를 저장하는
Member
엔티티 생성 - 회원 가입 양식을 저장하는
MemberFormDto
생성 - MEmber엔티티를 DB에 저장할
MemberRepository
생성하고,findByEmail()
을 만든다. (중복회원 검사시 사용) - Repository에 저장하는 메서드를 가진
MemberService
를 만든다.
-> MemberService
@Service
@Transactional
@RequiredArgsConstructor // final이나 @NonNull이 붙은 필드에 생성자를 생성
public class MemberService implements UserDetailsService {
//@Autowired 없이도 의존 주입가능!
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("이미 가입된 회원입니다.");
}
}
- 정상 작동하는 지
MemberServiceTest
를 한다. (회원가입과 중복회원방지가 되는지 확인) - 로직을 만들었기 때문에 이를 매핑할
MemberController
를 만든다. - 회원 가입을 위한 페이지를 만든다 (생략)
CSRF란?
사이트간 위조 요청으로 해커가 CRUD 행위를 웹사이트 요청하게 하는 공격
이를 막기 위해 CSRF 토큰을 이용해 서버가 허용한 요청이 맞는지 확인한다. - 회원가입 후 메인페이지로 이동하는 redirect를 만든다.
이제 회원 가입 시, 중복을 막기 위해 Validation
을 이용한다.
Gradle에서 Validation 사용하기
위 Annotation을 이용해서 각 칼럼의 값을 설정한다.
-> MemberFormDto
@Getter @Setter
public class MemberFormDto {
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String name;
@NotEmpty(message ="이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식으로 입력해주세요.")
private String email;
@NotEmpty(message="비밀번호는 필수 입력 값입니다.")
@Length(min=8, max=16, message="비밀번호는 8자 이상, 16자 이하로 입력해주세요")
private String password;
@NotEmpty(message = "주소는 필수 입력 값입니다.")
private String address;
}
이제 본격적으로 @Valid
를 이용한 중복을 막기위해 컨트롤러를 설정한다.
-> MemberController
@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;
@GetMapping(value = "/new")
public String memberForm(Model model){
model.addAttribute("memberFormDto", new MemberFormDto());
return "member/memberForm";
}
@PostMapping(value = "/new")
public String newMember(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){
if(bindingResult.hasErrors()){
return "member/memberForm";
}
try {
Member member = Member.createMember(memberFormDto, passwordEncoder);
memberService.saveMember(member);
} catch (IllegalStateException e){
model.addAttribute("errorMessage", e.getMessage());
return "member/memberForm";
}
return "redirect:/";
}
}
@RequiredArgsConstructor
: final이나 @NonNull이 붙은 필드에 생성자를 생성해줌.- 검증하려는 객체에
@Valid
를 붙이고, 검사후 결과를 저장할BindingResult
를 파라미터로 받는다. hasError() 시, 회원 가입 페이지로 이동.
4-11] 회원 가입 시 데이터 검증 결과
4. 로그인/회원가입
UserDetailsService 인터페이스
: DB에서 회원 정보를 가져오는 역할loadUserByUsername()
: 회원정보를 조회하여 사용자의 정보와 권한을 갖는 UserDetails 인터페이스 반환
UserDetails
: 회원 정보를 담기 위해 사용하는 인터페이스- 위 인터페이스 직접 구현 혹은 Security가 제공하는 User 클래스 사용
위 인터페이스를 이용해서 요청받은 이메일과 같은 회원을 DB에서 조회하고 반환해보자.
-> MemberService
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email);
if(member == null){
throw new UsernameNotFoundException(email);
}
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
loadUserByUsername()
: 유저의 정보를 불러와서 UserDetails로 리턴(UserDetailsService를 구현한 User 객체를 반환함.)- 시작은 builder(), 마무리는 build()
로그인 및 로그아웃 시 사용할 URL 및 인증 설정을 합니다.
-> SecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception { // http 요청에 대한 보안 설정
http.formLogin()
.loginPage("/members/login")
.defaultSuccessUrl("/")
.usernameParameter("email")
.failureUrl("/members/login/error")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
.logoutSuccessUrl("/")
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService)
.passwordEncoder(passwordEncoder());
}
loginPage
: 로그인 페이지 URL 설정defaultSuccessUrl
// 로그인 성공 시 이동할 URLusernameParameter
: 로그인 시 사용할 파라미터 이름 설정failureUrl
: 실패 시 이동할 URLlogoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
:로그아웃 페이지 URL 설정logoutSuccessUrl("/")
: 로그아웃 성공 시 이동할 URLconfigure()
: 스프링 시큐리티의 인증은 AuthenticationManager로 이루어짐 (AuthenticationManagerBuilder 는 AuthenticationManager을 만드는 객체)- userDetailsService를 구현하는 객체로 memberService 지정하고, passwordEncoder를 지정
로그인과 로그아웃 페이지를 생성하고, 컨트롤러에 매핑시킨다.
테스트 코드를 작성해서 로그인과 로그아웃이 정상작동하는지 점검한다.
먼저 test코드용 스프링 시큐리티를 dependency 추가한다.
https://mvnrepository.com/artifact/org.springframework.security/spring-security-test
MemberControllerTest에 로그인이 정상적으로 작동하는지 테스트 해보자
# MemberControllerTest
@Autowired
private MemberService memberService;
@Autowired
private MockMvc mockMvc;
@Autowired
PasswordEncoder passwordEncoder;
public Member createMember(String email, String password){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail(email);
memberFormDto.setName("홍길동");
memberFormDto.setAddress("서울시 마포구 합정동");
memberFormDto.setPassword(password);
Member member = Member.createMember(memberFormDto,passwordEncoder);
return memberService.saveMember(member);
}
@Test
@DisplayName("로그인 성공 테스트")
public void loginSuccessTest() throws Exception{
String email = "test@email.com";
String password = "1234";
this.createMember(email,password);
mockMvc.perform(formLogin().userParameter("email")
.loginProcessingUrl("/members/login")
.user(email).password("12345"))
.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
}
MockMvc
: 실제 객체와 비슷하지만 테스트에 필요한 기능만 가지는 가짜 객체
이를 통해 웹 브라우저에서 요청을 하는 것처럼 테스트 가능perform()
: MockMvc 실행andExpect
: 인증 완료시 테스트 코드를 통과시킴
5. 페이지 권한 설정하기
ADMIN
계정만 접근 가능한 상품 등록 페이지를 만들어보자
HTML을 생성 후 컨트롤러에 매핑한다.
ajax란?
빠르게 동작하는 동적인 웹 페이지를 만들기 위한 개발 기법의 하나입니다.
Ajax는 웹 페이지 전체를 다시 로딩하지 않고도, 웹 페이지의 일부분만을 갱신할 수 있습니다.
즉 Ajax를 이용하면 백그라운드 영역에서 서버와 통신하여, 그 결과를 웹 페이지의 일부분에만 표시할 수 있습니다.
( 이때 사용하는 객체가 XMLHttpRequest )
- ajax의 경우 http request header에 XMLHttpRequest라는 값이 세팅되어 요청이 옮
- 인증되지 않은 사용자가 ajax로 리소스 요청 시, Unauthorized 에러가 발생시키고 나머지는 로그인 페이지로 리다이렉트 함.
# CustomAuthenticationEntryPoint
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if("XMLHttpRequest".equals(request.getHeader("x-requested-with"))){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
} else{
response.sendRedirect("/members/login");
}
}
}
# SecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception { // http 요청에 대한 보안 설정
http.formLogin()
.loginPage("/members/login")
.defaultSuccessUrl("/")
.usernameParameter("email")
.failureUrl("/members/login/error")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
.logoutSuccessUrl("/")
;
http.authorizeRequests()
.mvcMatchers("/css/**", "/js/**", "/img/**").permitAll()
.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/**"); // static의 하위 파일은 인증을 무시하도록 설정
}
authorizeRequests()
: 시큐리티 처리에 httpServletRequest를 이용permitAll
: 모든 사용자가 인증없이 해당 경로에 접근할 수 있도록 설정hasRole()
: ADMIN만 접근할 수 있도록 설정.anyRequest().authenticated()
: 설정한 경로를 제외한 나머지 경로는 모두 인증을 요구하도록 설정
ADMIN으로 등록된 회원이 없기 때문에 회원 가입시 Role을 ADMIN으로 설정 후 회원가입을 하면 성공적으로 로그인 된다.
이제 테스트 코드를 작성해서 정상적으로 작동되는지 점검하자
# ItemControllerTest
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@DisplayName("상품 등록 페이지 권한 테스트")
@WithMockUser(username="admin", roles = "ADMIN")
public void itemFormTest() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")) // 상품등록 페이지에 GET 요청 보냄
.andDo(print()) // 요청과 응답 메시지를 콘솔에 출력
.andExpect(status().isOk()); // 응답 상태 코드가 정상인지 확인하고 맞으면 테스트 통과
}
@Test
@DisplayName("상품 등록 페이지 일반 회원 접근 테스트")
@WithMockUser(username="user", roles = "USER")
public void itemFormNotAdminTest() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new"))
.andDo(print())
.andExpect(status().isForbidden()); // 응답 상태 코드가 Forbidden 예외인지 확인하고 맞으면 테스트 통과
}
}
'🛠 백엔드 > 쇼핑몰 클론코딩' 카테고리의 다른 글
[2] JPA (0) | 2023.01.08 |
---|---|
[7] Order (0) | 2023.01.07 |
[6] Item 등록 및 조회 (0) | 2023.01.04 |
[5] 연관매핑 (0) | 2022.12.30 |
[3] Thymeleaf (0) | 2022.12.25 |