완성된 깃허브 코드 :
git clone https://github.com/gusrl6394/SpringbootShop
cd SpringbootShop
git reset --hard c4bcefd93950572032d14d51712e90e8671e187c
guid : Getting Started | Securing a Web Application (spring.io)
pom.xml
<!-- spring-boot-starter-security && spring-security-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
SecurityConfig.java
- @EnableWebSecurity : WebSecurityConfigurerAdapter 상속받고 해당 어노테이션을 선언하면 SpringSecurityFilterChain이 자동으로 포함된다
- WebSecurityConfigurerAdapter : 보안설정을 커스터마이징할 수 있다.
- configure(HttpScurity http) : http 요청에 대한 보안 설정
- PasswordEnncoder : 해당 함수를 이용하여 패스워드를 암호화 시킬수 있다.
package com.shop.config;
import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MemberService memberService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/members/login")
.defaultSuccessUrl("/")
.usernameParameter("email")
.failureUrl("/members/login/error")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
.logoutSuccessUrl("/");
http.authorizeRequests()
.mvcMatchers("/", "/members/**", "/item/**", "/images/**").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
Member.java 중 일부분
- passwordEncoder.encode 이용하여 패스워드를 암호화 시킨다.
public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder){
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
MemberService.java
- @Transactional : 비즈니스 로직을 담당하는 서비스 계층 클래스에 적용한다. 로직을 처리하다가 에러가 발생하였다면, 변경된 데이터를 로직을 수행하기 이전 상태로 콜백 시켜준다.
- @RequiredArgsConstructor : final 또는 @NonNull 이 붙은 필드에 생성자를 생성해 준다.
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
public Member saveMember(Member member){
validateDuplicatemember(member);
return memberRepository.save(member);
}
public void validateDuplicatemember(Member member){
Member findMember = memberRepository.findByEmail(member.getEmail());
if(findMember != null){
throw new IllegalStateException("이미 가입된 회원입니다.");
}
}
@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.getName())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
}
memberForm.html
- 스프링 시큐리티를 사용할 경우 기본적으로 CSRF를 방어하기 ㅜ이해 모든 POST 방식의 데이터 전송에는 CSRF 토큰 값이 있어야 한다.
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
유효값인지 확인하기 위한 라이브러리
<!-- spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
memberForm.html
<div layout:fragment="content">
<form action="/members/new" role="form" method="post" th:object="${memberFormDto}">
<div class="row g-3">
<div class="col-12">
<label th:for="name" class="form-label">이름</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요">
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect data</p>
</div>
<div class="col-12">
<label th:for="email" class="form-label">이메일주소</label>
<input type="email" th:field="*{email}" class="form-control" placeholder="이메일을 입력해주세요">
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect data</p>
</div>
<div class="col-12">
<label th:for="password" class="form-label">비밀번호</label>
<input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호 입력">
<p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect data</p>
</div>
<div class="col-12">
<label th:for="address" class="form-label">주소</label>
<input type="text" th:field="*{address}" class="form-control" placeholder="주소를 입력해주세요">
<p th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="fieldError">Incorrect data</p>
</div>
<div style="text-align: center">
<button type="submit" class="btn btn-primary" style="">Submit</button>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</div>
</form>
</div>
MemberController.java
@PostMapping(value = "/new")
public String memberForm(@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:/";
}
MemberFormDto.java
@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;
}
'2022 > JPA입문(完)' 카테고리의 다른 글
스프링 시큐리티 - 2 (0) | 2022.01.24 |
---|---|
부트스트랩(bootstrap) 5.1 적용 (0) | 2022.01.24 |
Thymeleaf - 1 (0) | 2022.01.21 |
Spring Data JPA - 2 (0) | 2022.01.20 |
Spring Data JPA - 1 (0) | 2022.01.18 |