-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
ahah525
commented
Nov 9, 2022
- [Feat] 공통 모듈(기반 코드) 작업 #2
- [Feat] 정산 데이터 Batch 생성, 출금 기능(3주차 추가 기능) #23
- [Feat] REST API 개발(4주차 필수 기능) #33
- [Test] REST API 개발 테스트 #34
- [Test] Ut 테스트 #35
- [Feat] JWT 세팅 #36
- [Feat] 4주차 추가 기능 #37
…(JWT 토큰)이 있는지 테스트
…sponseEntity<RsData>)
…httpHeadersOf() 도입
…rizationFilter 추가
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다양한 케이스에 대해서 테스트 코드 작성까지 작성하느라 수고많으셨습니다!!
저도 테스트 코드를 빠른 시일내에 작성해야겠네요!!
상태코드에 대해서 검증도 좋지만, 응답메시지에 대한 검증도 있으면 좋을 것 같습니다!
@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(); | ||
} | ||
} |
There was a problem hiding this comment.
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에서 가져오도록 수정해봐야겠어요👍
4Week_Record/4Week_한승연.md
Outdated
### 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
구현하신 JWT 프로세스가 깔끔하게 문서화되어 있어서 좋습니다! 저도 JWT 관련해서는 더 공부중인데 공부 후에 제가 구현한 프로세스 기록으로 남겨야겠네요!
4Week_Record/4Week_한승연.md
Outdated
**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` 세션 등록) | ||
--- |
There was a problem hiding this comment.
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()); | ||
|
||
// 본인이 소유한 도서인지 검증 |
There was a problem hiding this comment.
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());
}