-
Notifications
You must be signed in to change notification settings - Fork 1
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
[Feat] controller & service 테스트 코드 작성, [Feat] 시큐리티 & jwt 구현, 예외 필터 구현, [Docs] 7주차 발표자료 업로드 #54
base: main
Are you sure you want to change the base?
Changes from 1 commit
434f204
61dfb7e
38e6873
dceee50
846a683
1cefaf7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,11 @@ dependencies { | |
implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
implementation 'org.springframework.boot:spring-boot-starter-web' | ||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' // 접속 url: localhost:8080/swagger-ui/index.html | ||
implementation 'com.google.code.gson:gson' | ||
implementation 'com.google.code.gson:gson' // 객체를 json으로 변환해주는 구글의 라이브러리 | ||
implementation 'org.springframework.boot:spring-boot-starter-security' | ||
// JWT | ||
implementation 'io.jsonwebtoken:jjwt:0.9.1' | ||
implementation 'javax.xml.bind:jaxb-api:2.3.1' //추가하지 않으면 tokenProvider에서 에러 남 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 나는 이런 의존성을 넣은적이 없어서 그런데, 오류가 났을 때 해당 의존성을 넣으라고 한 레퍼런스를 알려줄 수 있어? 그리고 해당 의존성이 어떤 역할을 수행해? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이게 저 패키지안에 있는 javax.xml.bind.DatatypeConverter 클래스를 이용해서, 저게 없으면 직접 base64로 secretkey를 디코딩하고 서명에 쓸 키를 따로 만들어줘야하더라. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 나는 저 의존성을 넣지 않고 base64로 시크릿 키를 인코딩해도 잘 동작했어서 질문한 거였어 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음.. 버전이 낮아서 그런건가? |
||
compileOnly 'org.projectlombok:lombok' | ||
runtimeOnly 'com.h2database:h2' | ||
annotationProcessor 'org.projectlombok:lombok' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ public enum StatusCode { | |
/* 2xx: 성공 */ | ||
// Member | ||
MEMBER_CREATE(CREATED,"회원 가입 완료"), | ||
MEMBER_LOGIN(OK,"로그인 완료"), | ||
// TodoList | ||
TODO_CREATE(CREATED,"할 일 생성 완료"), | ||
TODO_FOUND(OK,"할 일 조회 완료"), | ||
|
@@ -24,8 +25,14 @@ public enum StatusCode { | |
COMMENT_DELETE(OK,"할 일 삭제 완료"), | ||
|
||
/* 400 BAD_REQUEST : 잘못된 요청 */ | ||
LOGIN_ID_INVALID(BAD_REQUEST,"아이디가 틀렸습니다."), | ||
EMAIL_INVALID(BAD_REQUEST,"이메일이 틀렸습니다."), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아이디 / 비밀번호가 틀렸다고 했을 때 어떤 부분이 틀렸다고 알려주는 게 적합할까? 한 번 고민해봐 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞아. 이게 내가 프론트를 모르다보니까 주는 정보를 어디까지 한정해야하는지 고민이 되더라고. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 토큰을 활용한 인증에서의 문제와 내가 여기서 잡은 문제는 약간 다르다고 생각해. 여기 내용은 그 내용이 아니라 틀린 부분을 알려주면 안된다는 의도에서 질문을 한거였어 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 글쿤.. |
||
INPUT_VALUE_INVALID(BAD_REQUEST,"유효하지 않은 입력입니다."), | ||
PASSWORD_INVALID(BAD_REQUEST,"비밀번호가 틀렸습니다."), | ||
|
||
|
||
/* 401 UNAUTHORIZED : 비인증 사용자 */ | ||
ACCESS_TOKEN_INVALID(UNAUTHORIZED,"access 토큰이 유효하지 않습니다."), | ||
|
||
|
||
/* 404 NOT_FOUNT : 존재하지 않는 리소스 */ | ||
MEMBER_NOT_EXIST(NOT_FOUND,"존재하지 않는 멤버입니다."), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 requestMatchers로 로그인하지 않아도 접근할 수 있는 엔드포인트에 대한 정의를 하는데, 만약 이러한 엔드포인트가 여럿 생긴다고 한다면 코드에 몇줄씩 추가하는 방향으로 밖에 구현이 안되는걸까? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러게. 이건 고쳐봐야겠다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package com.appcenter.practice.config; | ||
|
||
import com.appcenter.practice.exception.JwtExceptionFilter; | ||
import com.appcenter.practice.security.JwtAuthenticationFilter; | ||
import com.appcenter.practice.security.JwtTokenProvider; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; | ||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||
import org.springframework.security.config.http.SessionCreationPolicy; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; | ||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
private final JwtTokenProvider jwtTokenProvider; | ||
|
||
@Bean | ||
PasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
|
||
@Bean | ||
public WebSecurityCustomizer webSecurityCustomizer() { | ||
return (web) -> web.ignoring() | ||
.requestMatchers(PathRequest.toH2Console()); | ||
|
||
} | ||
|
||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { | ||
|
||
http | ||
.authorizeHttpRequests(authorizeRequest -> authorizeRequest | ||
.requestMatchers(new MvcRequestMatcher(introspector, "/members/**")).permitAll() | ||
.requestMatchers(new MvcRequestMatcher(introspector, "/swagger-ui/**")).permitAll() | ||
.requestMatchers(new MvcRequestMatcher(introspector, "/v3/api-docs/**")).permitAll() | ||
//스프링 시큐리티는 자동으로 Role_접두어를 붙여준다. | ||
.requestMatchers(new MvcRequestMatcher(introspector, "/**")).hasRole("USER") | ||
.anyRequest().authenticated()) | ||
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) | ||
.addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); | ||
|
||
|
||
|
||
|
||
//csrf:사이트간 위조 요청, disalbel 한 이유: jwt는 서버에 인증정보를 저장하지 않기 때문에 필요 없음. | ||
http.csrf(AbstractHttpConfigurer::disable); | ||
http.cors(AbstractHttpConfigurer::disable); | ||
return http.build(); | ||
|
||
} | ||
} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엔드포인트 형식을 고치면서 prefix (comments)를 requestMapping으로 빼주면 좋을 것 같아! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 협업 투두리스트 만들 때 이건 고쳤어! 같은 테이블의 id를 사용할 땐 pathvariable로 얻어오고, 다른 테이블의 id가 필요하면 requestparam으로 받아오게끔 했어. 이러면 uri가 좀 깔끔해지더라고. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,21 @@ | ||
package com.appcenter.practice.controller; | ||
|
||
import com.appcenter.practice.common.StatusCode; | ||
import com.appcenter.practice.dto.reqeust.member.LoginMemberReq; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 패키지 경로에 request가 오타가 났네 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 땡큐 ㅋㅋ 아예 몰랐네. |
||
import com.appcenter.practice.dto.reqeust.member.SignupMemberReq; | ||
import com.appcenter.practice.dto.response.CommonResponse; | ||
import com.appcenter.practice.service.MemberService; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import static com.appcenter.practice.common.StatusCode.MEMBER_CREATE; | ||
import static com.appcenter.practice.common.StatusCode.MEMBER_LOGIN; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/members") | ||
|
@@ -22,8 +26,17 @@ public class MemberController { | |
@PostMapping | ||
public ResponseEntity<CommonResponse<Long>> signup( @RequestBody @Valid SignupMemberReq reqDto){ | ||
return ResponseEntity | ||
.status(StatusCode.MEMBER_CREATE.getStatus()) | ||
.body(CommonResponse.of(StatusCode.MEMBER_CREATE.getMessage(), memberService.signup(reqDto))); | ||
.status(MEMBER_CREATE.getStatus()) | ||
.body(CommonResponse.of(MEMBER_CREATE.getMessage(), memberService.signup(reqDto))); | ||
} | ||
|
||
@PostMapping(value = "/login") | ||
public ResponseEntity<CommonResponse<Void>> login(@RequestBody @Valid LoginMemberReq signInMemberReqDto){ | ||
|
||
return ResponseEntity.ok() | ||
.header(HttpHeaders.AUTHORIZATION, memberService.login(signInMemberReqDto)) | ||
.body(CommonResponse.of(MEMBER_LOGIN.getMessage(), null)); | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.appcenter.practice.dto.reqeust.member; | ||
|
||
import com.appcenter.practice.domain.Member; | ||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.Pattern; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@NoArgsConstructor | ||
public class LoginMemberReq { | ||
|
||
@Email(message = "잘못된 이메일 형식입니다.") | ||
private String email; | ||
|
||
@Pattern(message = "비밀번호는 8자 이상의 영어, 숫자, @,$,!,%,*,#,?,& 중 하나 이상을 포함한 특수문자로 이루어져야 합니다." | ||
, regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$") | ||
private String password; | ||
|
||
public Member toEntity(){ | ||
return Member.builder() | ||
.email(email) | ||
.password(password) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.appcenter.practice.exception; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
|
||
@Component | ||
public class JwtExceptionFilter extends OncePerRequestFilter { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AuthenticationFilter의 doFilterInternal과 ExceptionFilter의 doFilterInternal의 차이는 뭐고, 형의 security config에서의 필터 구조는 어떻게 되는거야? 두 필터를 다 거치는거야? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AuthenticationFilter는 토큰을 받아서 유효성 검증을 하고 인증객체를 만들어서 세션 저장소에 저장을 하는거고, |
||
|
||
/* | ||
인증 오류가 아닌, JwtAuthenticationFilter에서 일어나는 오류는 이 필터에서 따로 잡아낸다. | ||
이를 통해 JWT 관련 에러와 인증 에러를 따로 잡아낼 수 있다. | ||
*/ | ||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { | ||
try { | ||
chain.doFilter(request, response); // JwtAuthenticationFilter로 이동 | ||
} catch (CustomException ex) { | ||
// JwtAuthenticationFilter에서 예외 발생하면 바로 setErrorResponse 호출 | ||
sendErrorResponse(response,ex); | ||
} | ||
} | ||
|
||
private void sendErrorResponse(HttpServletResponse response, CustomException ex) throws IOException { | ||
//응답 상태코드에 status를 json으로 보냄. | ||
response.setStatus(ex.getStatusCode().getStatus().value()); | ||
response.setContentType("application/json"); | ||
response.setCharacterEncoding("UTF-8"); | ||
//json으로 보낼 body에 해당하는 포맷을 설정함. | ||
//response.getWriter().write(...)는 내부적으로 flush()가 발생해서 따로 호출할 필요 없음. | ||
response.getWriter().write(String.format("{\"message\": \"%s\"}", ex.getStatusCode().getMessage())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 호출할 필요는 없지만 호출한 이유는 뭘 까..?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 저게 flush를 따로 호출할 필요가 없다는 뜻이었어. 고쳐야겠다. |
||
} | ||
} |
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.
해당 의존성은 테스트코드만을 위해서 사용되는거야?
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.
맞아. http 요청을 시뮬레이션해야하니까, json 데이터를 본문에 넣어야하기 떄문에 json으로 변환해주는 클래스를 썼어.