LostCatBox

SpringProject-Board-CH10

Word count: 1.3kReading time: 7 min
2022/12/24 Share

Spring 게시판 프로젝트 10편 (각종 이슈들 개선점 정리)

Created Time: August 19, 2022 1:19 AM
Last Edited Time: August 24, 2022 10:05 PM
Tags: Java, Spring, Computer

왜?

프로젝트의 부족한 부분들이 많이 보임

보안관련 이슈 등등

logout 구현

구현

  • securityconfig설정중
1
2
3
4
5
.logout()
.logoutUrl("/logout")
.clearAuthentication(true)//로그 아웃시 인증정보를 지움
.invalidateHttpSession(true) //세션을 무효화 시킨다
.and()

문제점

200

logout을 method:post 를 요청할때 CORS이슈가 터졌다.

분명 logout을 method:post로 동작하였다

하지만 계속해서 CORS이슈가 터졌다.

이유?

CORS는 결국 헤더에 allowedHeader가 적혀있지 않은 response를 받아서 브라우져에서 경고를 띄우는것이다.

원인은 바로 SecurityConfig에서 logout()을 설정해놓았고 logout()이 성공하면 자동으로 logoutsuccessurl()을 통해 자동으로 redirect가 되기때문이다. 하지만 이 redirect가 되는 페이지는 allowedHeader 옵션이 없었기 때문에 CORS이슈가 터진것이다.

1
2
3
4
5
// Custom logout handler only exists to handle a CORS problem with /logout
// where spring-session processes the logout request/response before it gets
// to the CORS filter so it doesn't get the allow-origin header which then
// causes the browser to reject the /logout response. Manually set the
// allow-origin header in the logout handler and Bob's your uncle.

해결책

생각해볼수있는 해결책은 두가지다

  • logoutSuccessHandler를 활용하는 방법(이는 logoutsuccessurl()이 동작하지않음)
    .logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.*OK*)))
    하지만 이방법은 핸들러가 응답해주는 header에는 allowedheader가 포함되어있지않기때문에 따로 핸들러나 응답을 직접 클래스로 구현해서 응답해줘야한다.
  • 새로이 LogoutSuccessHandler(allow-origin정보를 넣어주는역할)를 만들어주고,
    security에서 .logoutSuccessHandler((new MyLogoutSuccessHandler()))커스텀 successhadler호출
1
2
3
4
5
6
7
8
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*"); //cors이슈 위한설정
}
}

PostResponseDto제거

PostResponseDto는 comments를 함께 해당post 인스턴스를 view를 render할때쓰는 model에 넣어 한번에 전달하기위해 활용했지만, front-end와 backend를 나눠서 posts와 comments에 각각 요청을 하게 되었고, 따라서 PostResponseDto를 PostDto로 모두 전환후 PostResponseDto는 해당 프로젝트에서 제거하였다.

1
2
3
4
5
//PostResponseDto에서
//정의부분에서 제거
private List<CommentDto> comments;
//생성자에서 제거
this.comments = post.getComments().stream().map(CommentDto::new).collect(Collectors.toList());

HttpStatus 알맞게 조정

client 에서는 server error인 500을 보게 해서는 안되는데, 나는 모든 에러를 모두 500대로 하고있었다.

각각 HttpStatus에 알맞게

400 Bad Request

401 권한 오류 (적당한 권한을 가진애로가져와라

403 숨기고싶은것 (금지된항목, 권한으로 해결되지않은 항목)

404 NotFound(API에서 공개하지않았고, 찾아지지 않았을때 보여주느것)

등으로 각자 알맞게 사용하여 변경하였다

https://stackoverflow.com/questions/1959947/whats-an-appropriate-http-status-code-to-return-by-a-rest-api-service-for-a-val

Server Side 이슈 처리

문제점

  • post를 등록시 10자이상의 제목을 입력시 DB의 스펙과 맞지않으므로 sql error가 뜨면서 httpstatus 500 응답되었다.

500

해결법

Client의 데이터는 조작이 쉬울 뿐더러 모든 데이터가 정상적인 방식으로 들어오는 것도 아니기 때문에, Client Side뿐만 아니라 Server Side에서도 데이터 유효성을 검사해야 할 필요가 있다.

따라서 기본적으로 post, comment에 대해서 validation처리를 할 필요가있었다.

스프링부트 프로젝트에서는 @validated
를 이용해 유효성을 검증에 이용하였고,

@Validated
로 검증한 객체가 유효하지 않은 객체라면 Controller
의 메서드의 파라미터로 있는 BindingResult
인터페이스를 확장한 객체로 들어옵니다. >>> bindingResult.hasError()를 활용하여, 바로 처리하자

아래는 예시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@PostMapping
public ResponseEntity<?> createUSer(@Validated @RequestBody final UserCreateRequestDto userCreateRequestDto, BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).collect(Collectors.toList());
// 200 response with 404 status code
return ResponseEntity.ok(new ErrorResponse("404", "Validation failure", errors));
// or 404 request
// return ResponseEntity.badRequest().body(new ErrorResponse("404", "Validation failure", errors));
}
try {
final User user = userService.searchUser(userCreateRequestDto.toEntity().getId());
}catch (Exception e){
return ResponseEntity.ok(
new UserResponseDto(userService.createUser(userCreateRequestDto.toEntity()))
);
}
// user already exist
return ResponseEntity.ok(
new UserResponseDto(userService.searchUser(userCreateRequestDto.toEntity().getId()))
);

}

내가 활용한 방법

  • controller에서 post @validated 선언 Dto로 전환과정에서 에러시 bindingResult에서 hasErrors() 있을때 CustomException만들어서 보냄
1
2
3
4
5
6
7
8
public SingleResult<PostDto> create(@Validated @RequestBody PostDto postDto, BindingResult bindingResult){
if (bindingResult.hasErrors()){
List<String> errors = bindingResult.getAllErrors().stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
throw new DataFieldInvalidCException(errors);
}
String email = getCurrentUserEmail();
return responseService.getSingleResult(postService.savePost(email, postDto));
}
  • PostDto
    • @size(max=10) 등등 다양한 기능을 validation으로 활용할수있다. 에너테이션 찾아보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package hello.postpractice.domain;

import com.sun.istack.NotNull;
import lombok.*;

import javax.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;

//Controller와 Service 사이에서 데이터를 주고받는 DTO를 구현한다
@Getter
@Setter
@ToString
@NoArgsConstructor
public class PostDto {
private Long id;
@Size(max=10)
private String postName;
@Size(max=200)
private String content;
private User user;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;

public Post toEntity(){
Post post = Post.builder()
.id(id)
.postName(postName)
.content(content)
.user(user)
.build();
return post;
}

public PostDto(Post post){
this.id = post.getId();
this.postName = post.getPostName();
this.content = post.getContent();
this.createdDate = post.getCreatedDate();
this.modifiedDate = post.getModifiedDate();
this.user = post.getUser();
}
}
  • CustomException
1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
public class DataFieldInvalidCException extends RuntimeException{
public DataFieldInvalidCException(String message) {
super(message);
}
public DataFieldInvalidCException(List messageList) {
super((String)messageList.stream().collect(Collectors.joining(","))); // List Stream을 String으로 join
}

}

Auth 구현(Admin,User)

다수 Role 적용

SpringSecurity에서 Role은 Role_Admin, ROLE_USER을 동시에 갖는것을 목표로하였다.

변경한점

User Entity

  • SpringSecurity에서 권한확인시 UserDetail통해 getAuthorities()가 항상 호출된다. 따라서 저장하는 방법은 String으로 저장하되, getAuthorities() 호출시에는 GrantedAuthority로 구성된 Collection의 구현체를 반환해야한다. 따라서 아래와 같이 구현하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User extends BaseTimeEntity implements UserDetails {
...
@Column(name = "auth")
private String auth;

// 사용자의 권한을 콜렉션 형태로 반환
// 단, 클래스 자료형은 GrantedAuthority를 구현해야함
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
for (String role : auth.split(",")) {
roles.add(new SimpleGrantedAuthority(role));
}
return roles;
}
...
}

SignController

  • signupMap.get()활용시 값이 없을 경우, 값이 들어오지 않았을경우 default값으로 처리
  • validation을 이용하여 아래와같이 개선하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SignController {
...
@PostMapping("/signup")
public SingleResult<Long> signup(@RequestBody Map<String, String> signupMap){
String email = signupMap.get("email");
String password = signupMap.get("password");
String nickname = signupMap.get("nickname");
String auth = signupMap.get("auth");
if (auth.isEmpty()) {
auth = "ROLE_USER"; //""이라면 ROLE_USER 로 default
} else {
auth = signupMap.get("auth"); //"ROLE_USER,ROLE_ADMIN" 이런식으로 들어옴
}

UserSignupRequestDto userSignupRequestDto = UserSignupRequestDto.builder()
.email(email)
.password(passwordEncoder.encode(password))
.nickname(nickname)
.auth(auth)
.build();
Long signupId = userService.signup(userSignupRequestDto);
return responseService.getSingleResult(signupId);
}
...
}
  • 위 형태를 validation으로 축소시킴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/signup")
public SingleResult<Long> signup(@Validated @RequestBody UserSignupRequestDto requestuserSignupRequestDto, BindingResult bindingResult){
if (bindingResult.hasErrors()){
List<String> errors = bindingResult.getAllErrors().stream().map(e->e.getDefaultMessage()).collect(Collectors.toList());
throw new DataFieldInvalidCException(errors);
}
UserSignupRequestDto userSignupRequestDto = UserSignupRequestDto.builder()
.email(requestuserSignupRequestDto.getEmail())
.password(passwordEncoder.encode(requestuserSignupRequestDto.getPassword())) //password 암호화시 password이런식으로 다시 꺼내와줘야하나?
.nickname(requestuserSignupRequestDto.getNickname())
.auth(requestuserSignupRequestDto.getAuth())
.build();
Long signupId = userService.signup(userSignupRequestDto);
return responseService.getSingleResult(signupId);
}

UserSignupRequestDto

  • User 저장시 사용하는 UserDto를 반드시 알맞게 수정
  • @validation 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class UserSignupRequestDto {

@NotEmpty
@Size(max=30)
private String email;

@NotEmpty
@Size(max=30)
private String password;
@Size(max=30)
private String nickname;

@NotEmpty
private String auth;

@Builder
public UserSignupRequestDto(String email, String password, String nickname, String auth) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.auth = auth;
}

public User toEntity() {
return User.builder()
.email(email)
.password(password)
.nickname(nickname)
.auth(auth)
.build();
}
}
CATALOG
  1. 1. Spring 게시판 프로젝트 10편 (각종 이슈들 개선점 정리)
  2. 2. 왜?
  3. 3. logout 구현
    1. 3.1. 구현
    2. 3.2. 문제점
    3. 3.3. 이유?
    4. 3.4. 해결책
  4. 4. PostResponseDto제거
  5. 5. HttpStatus 알맞게 조정
  6. 6. Server Side 이슈 처리
    1. 6.1. 문제점
    2. 6.2. 해결법
    3. 6.3. 내가 활용한 방법
  7. 7. Auth 구현(Admin,User)
    1. 7.1. 다수 Role 적용
    2. 7.2. 변경한점
      1. 7.2.1. User Entity
      2. 7.2.2. SignController
      3. 7.2.3. UserSignupRequestDto