Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4Week_Mission] 한승연 - Jwt-Security 로그인 구현, REST API 개발, SpringDoc #38

Merged
merged 39 commits into from
Nov 18, 2022

Conversation

Copy link

@minnseong minnseong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4주차까지 수고 많으셨습니다!

private final PasswordEncoder passwordEncoder;

@PostMapping("/login")
public ResponseEntity<RsData> login(@RequestBody LoginDto loginDto) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

많은 예외 사항에 대해서 고려해서 처리하신 거 잘하신 것 같습니다.!
하지만 만약 다른 컨트롤러에서는 여기보다 더 많은 예외를 처리할 수도 있을 것 같은데, 이럴 경우는 예외를 공통으로 처리하는 RestControllerAdvice를 사용하는 것이 좋아보입니다. RestControllerAdvice는 예외가 발생했을 때 처리하는 것이므로 컨트롤러에서 예외를 검증하지 않고, 서비스단에서 예외가 발생하였을 때 예외를 던지면 RestControllerAdvice가 처리해주기 때문에 컨트롤러 단이 많이 깔끔해질 수 있을 것 같습니다!

@NotBlank(message = "password 을(를) 입력해주세요.")
private String password;

public boolean isNotValid() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기론 dto에는 Getter, Setter를 제외하고 다른 로직이 포함되면 안되는 것로 알고 있습니다.
다만, 안되는 것은 아니지만 dto는 Getter, Setter만 가지고 있는 순수 객체로 만드는 것을 권장하는 걸로 알고 있습니다!

private boolean emailVerified;
private String nickname;

public static MemberDto toDto(Member member) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 static 메서드를 통해서 엔티티와 Dto 매핑을 잘 해주신 것 같습니다!

@@ -157,4 +161,25 @@ public void createBankInfo(Member member, WithdrawAccountForm withDrawAccountFor
// TODO: 계좌 정보는 memberContext 값에 담겨있지 않으므로 세션값 강제 수정할 필요X
//forceAuthentication(member);
}

// AccessToken 발급(발급된게 있으면 바로 리턴)
@Transactional

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적인 생각으로는 jwt관련 코드와 memberService와는 분리하는 것이 좋은 것 같습니다.
jwt관련 코드를 수정해야할 때 memberService를 수정해야하는 일이 발생하기 때문입니다.

@AutoConfigureMockMvc
@Transactional
@ActiveProfiles("test")
class MemberApiControllerTest {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다양한 케이스에 대해서 테스트 코드 작성까지 작성하느라 수고많으셨습니다!!
저도 테스트 코드를 빠른 시일내에 작성해야겠네요!!

상태코드에 대해서 검증도 좋지만, 응답메시지에 대한 검증도 있으면 좋을 것 같습니다!

Comment on lines +12 to +34
@Getter
@Setter
@Builder
@AllArgsConstructor
public class MyBookDto {
private Long id;
private LocalDateTime createDate;
private LocalDateTime modifyDate;
private Long ownerId;
private ProductDto product;

public static MyBookDto toDto(MyBook myBook) {
ProductDto productDto = ProductDto.toDto(myBook.getProduct());

return MyBookDto.builder()
.id(myBook.getId())
.createDate(myBook.getCreateDate())
.modifyDate(myBook.getUpdateDate())
.ownerId(myBook.getOwner().getId())
.product(productDto)
.build();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컬럼들간의 연관관계 때문에 mybook에서 순환참조 문제가 발생했었는데 이렇게 DTO로 변환시켜서 순환참조 일어날 수 있는 외래키 컬럼(ex.product)들도 각 Dto에서 가져오도록 수정해봐야겠어요👍

Comment on lines 78 to 85
### JWT 프로세스
1. 사용자가 `username, password` 를 입력하고 서버로 로그인 요청을 보낸다.
2. 로그인 성공시 서버는 비밀키로 서명을 하고 공개키로 암호화 하여 `Access Token` 을 발급한다.
3. `Authorization Header` 에 `Access Token` 을 담아 클라이언트에게 응답을 보낸다.
4. 클라이언트는 API를 요청할 때 `Authorization Header` 에 `Access Token` 을 담아 요청을 보낸다.
5. 서버에서는 `Access Token` 을 검증하고 사용자를 인증한다.
6. 서버가 요청에 대한 응답을 클라이언트에게 전달한다.
[참고](https://velog.io/@junghyeonsu/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현하신 JWT 프로세스가 깔끔하게 문서화되어 있어서 좋습니다! 저도 JWT 관련해서는 더 공부중인데 공부 후에 제가 구현한 프로세스 기록으로 남겨야겠네요!

Comment on lines 179 to 262
**6. REST API 요청 Security 설정**
- REST API 요청이 들어왔을 때는 JWT 방식으로 인증을 수행해야하므로 `ApiSecurityConfig` 에 관련 설정을 추가한다.
```java
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
...
.authorizeRequests(
authorizeRequests -> authorizeRequests
// 로그인 요청 외 모든 요청은 로그인 필수
.antMatchers("/api/*/member/login").permitAll()
.anyRequest()
.authenticated() // 최소자격 : 로그인
)
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(STATELESS)
)
.addFilterBefore(
jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class
)
...
return http.build();
}
```

- `/api/*/member/login` 요청 외 모든 `/api/**` 요청은 인증된 사용자여야 한다.
- 지정된 필터보다 먼저 실행되도록 `jwtAuthorizationFilter` (커스텀 필터) 를 추가한다.

**7. jwtAuthorizationFilter 추가**
- REST API 요청이 Controller 에 도달하기 이전에 앞 단(Filter 혹은 Interceptor)에서 인증/인가를 수행한다.
```java
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final MemberService memberService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String barerToken = request.getHeader("Authorization");
// 1. 1차 체크(정보가 변조되지 않았는지 검증)
if(barerToken != null) {
// accessToken에서 회원 정보 가져오려면 Authentication에서 Bearer 제거 필요
String token = barerToken.substring("Bearer ".length());
// 토큰이 유효하면 회원 정보 얻어서 강제 로그인 처리
if(jwtProvider.verify(token)) {
Map<String, Object> claims = jwtProvider.getClaims(token);
String username = (String) claims.get("username");
Member member = memberService.findByUsername(username);

// 2. 2차 체크(해당 엑세스 토큰이 화이트 리스트에 포함되는지 검증)
if (memberService.verifyWithWhiteList(member, token)) {
// 강제 로그인 처리
forceAuthentication(member);
}
}
}
filterChain.doFilter(request, response);
}

// 강제 로그인 처리
private void forceAuthentication(Member member) {
MemberContext memberContext = new MemberContext(member);

UsernamePasswordAuthenticationToken authentication =
UsernamePasswordAuthenticationToken.authenticated(
memberContext,
null,
member.getAuthorities()
);

// 이후 컨트롤러 단에서 MemberContext 객체 사용O
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
```
1. 요청 헤더의 `Access Token` 을 검증한다.
2. 토큰으로부터 `claim(회원 정보)` 를 얻어 DB에서 Member 객체 조회한다.
3. 해당 AccessToken 이 화이트 리스트에 포함되는지 검증한다.
4. 해당 회원 강제 로그인 처리한다.(`MemberContext` 세션 등록)
---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REST API와 jwt 로그인 방식이 추가되면서 필요했던 설정들도 기록해주셔서 나중에 다시 봐도 참고하기 좋을 것 같습니다 👍

.map(postHashTag -> postHashTag.getPost())
.collect(Collectors.toList());

// 본인이 소유한 도서인지 검증
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

본인이 소유한 도서인지 검증하는 로직을 계획하셨던 것 같은데 이 부분이 빠진 것 같습니다! 저 같은 경우 memberContext에서 받아온 member와 도서의 member를 비교하는 로직을 서비스단에 구현해서 컨트롤러에서 이를 참조하여 비교할 수 있도록 해주었습니다!

// ApiMyBookController.showDetail()

   if(myBookService.actorCanSee(member,myBook) == false) {
            return Ut.spring.responseEntityOf(RsData.of("F-4", "해당 회원은 도서 상품을 조회할 권한이 없습니다."));
        }
// MyBookService.java
    
public boolean actorCanSee(Member actor, MyBook myBook) {
        return actor.getId().equals(myBook.getMember().getId());
    }

@ahah525 ahah525 merged commit 49df4c5 into main Nov 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants