게시물 번호 = 게시물 전체 개수 - (게시물 페이지 * 페이지당 게시물 개수) - 나열 인덱스
<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
text-danger
: 빨간색small
: 작게ms-2
: 왼쪽 여백th:if
로 답변이 있는지 확인하고 있다면, 답변 개수 표시<span class="text-danger small ms-2"
th:if="${#lists.size(question.answerList) > 0}"
th:text="${#lists.size(question.answerList)}">
</span>
스프링 시큐리티는 스프링 기반 프레임워크의 인증과 권한을 담당하는 스프링 하위 프레임워크이다.
스프링 시큐리티는 기본적으로 인증되지 않은 사용자는 접속 할 수 없게 설정되어 있으므로, 시큐리티 설정을 통해 인증에 대한 설정을 해야 한다.
인증
: 로그인권한
: 인증된 사용자가 어떤 것을 할 수 있는지@Configuration
: 스프링 환경설정 파일임을 의미한다.@EnableWebSecurity
: 모든 요청 url이 스프링 시큐리티의 제어를 받는다.SpringSecurityFilterChain
이 동작하여 url 필터가 동작한다. SecurityFilterChain
을 Bean
으로 등록하여 시큐리티 세부 설정이 가능하다.@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(
new AntPathRequestMatcher("/**")).permitAll()
;
return http.build();
}
}
하지만, 위와 같이 설정해도 페이지에 접근할 수 없다.
왜냐하면, 스프링 시큐리티를 적용하면 CSRF
가 동작하기 때문이다.
CSRF
웹페이지 취약점 공격을 방지하기 위한 기술.
스프링 시큐리티에 의해 세션에서 토큰이 발급되고, 이후에 폼 전송 시 해당 토큰 값을 통해 유효한 요청인지 스프링 시큐리티를 통해 확인한다.
csrf
검증을 진행하지 않게 잠시 수정하였다. (아예 페이지 접근을 할 수 없으므로)@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers("/**").permitAll().and()
.csrf()
.ignoringRequestMatchers(new AntPathRequestMatcher("/**"));
return http.build();
}
}
username
과 email
을 Unique
로 지정해 유저 간 중복 저장이 불가능하게 만들었다.@Entity
@Getter
@Setter
public class SiteUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@Column(unique = true)
private String email;
}
public interface UserRepository extends JpaRepository<SiteUser, Long> {
}
BCryptPasswordEncoder
객체를 반환하는 PasswordEncoder
객체를 빈으로 추가 @Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
passwordEncoder
를 주입받아 비밀번호를 암호화했다.@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public SiteUser create(String username, String password, String email) {
SiteUser user = new SiteUser();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEmail(email);
userRepository.save(user);
return user;
}
}
UserCreateForm
생성password1
과 password2
를 만들어 두번 입력@Email
은 해당 형식이 이메일 형식인지 검증한다.@Getter
@Setter
public class UserCreateForm {
@Size(min = 3, max = 25)
@NotEmpty(message = "사용자ID는 필수항목입니다.")
private String username;
@NotEmpty(message = "비밀번호는 필수항목입니다.")
private String password1;
@NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
private String password2;
@NotEmpty(message = "이메일은 필수항목입니다.")
@Email
private String email;
}
/signup
url
을 통해 회원가입 폼으로 이동/signup
post
요청bindingResult
객체에 error
가 담겼으면 회원가입 폼으로 다시 이동userCreateForm
을 통해 받은 데이터로 user
생성 및 저장DataIntegrityViolationException
이 발생한다.bindingResult.reject
는 특정 필드의 오류가 아닌 일반적인 오류를 등록할때 사용한다.@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping("/signup")
public String signup(@ModelAttribute UserCreateForm userCreateForm) {
return "/user/signup_form";
}
@PostMapping("/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "/user/signup_form";
}
if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
bindingResult.rejectValue("password2", "passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "/user/signup_form";
}
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:/";
}
}