-
Notifications
You must be signed in to change notification settings - Fork 4
06 ‐ 03 ‐ Security Wiki
Ryu(Paul) edited this page Nov 9, 2023
·
1 revision
- 회원가입
- 이메일로 코드를 전송
- 이메일 인증 코드를 받아서 인증
- 로그인(JWT 인증 사용)
- 로그아웃(JWT 인증 사용)
- Access token을 재발급
- Refresh token을 redis에 저장하고 만료되면 자동삭제 적용
- OAuth2로 소셜 로그인
- 회원가입시, 비빌번호 2중체크 기능 추가
- email, nickname 중복체크 API를 따로 제작
- 프론트에서 AccessToken, RefreshToken 만료 체크
- 이부분은 정확하게 논의 되지 않았습니다.
- 프론트에서 만료 체크를 하는 이유는 로컬스토리지나 쿠키에 들어있는 token은 백엔드에서 확인이 불가능하며, 거기에 더해 여러번 핑퐁되기 때문입니다.
- email로 전송하는 code를 redis에 저장
- 유효시간을 구현하기 위함과, 여러 코드들을 저장해야하기 때문에 Redis를 사용하기로 했습니다. 다만, 프론트와는 아직 이야기가 되지 않았습니다.
- 탈퇴기능
- user, admin 등 securityConfig 권한 설정
- 회원가입을 진행하는 API입니다.
- 작성자 juhyelee
- MemberController를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<Object> signUp(@Valid @RequestBody UserInfo info)
- EmailAuthService를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public Message signUp(UserInfo info)
- 회원 정보들을 가져와 DB에서 동일한 닉네임, 동일한 이메일이 있는지 확인합니다.
- 동일한 닉네임, 동일한 이메일이 있는 경우 Message를 통해 HttpStatus.UNAUTHORIZED가 전달됩니다.
- 동일한 닉네임, 동일한 이메일이 없는 경우 User로 변환해 DB에 저장합니다.
- 프론트와 논의된 사항
- password를 프론트에서 확인한 이후, 서버에서 다시 확인해야 합니다. 이를 위한 API가 필요합니다.
- EmailAuthService에 동일한 닉네임이 있는지 확인하는 기능을 다른 API로 구현해야합니다.
- EmailAuthService에 동일한 이메일이 있는지 확인하는 기능을 다른 API로 구현해야합니다.
- 회원가입 정보 중에 reCAPCHA 인증을 위한 토큰이 전달됩니다. 이 토큰을 이용해 reCAPCHA 서버에게 인증 결과를 요청하고, 그 결과를 통해 회원가입을 계속 진행할지 판단 해야 합니다.
- 프론트에서 건의한 사항
- 패스워드 확인 API를 따로 만들었을 때, 해당 패스워드 확인 시에 입력한 패스워드와 회원가입을 진행 하는 API의 패스워드가 일치해야합니다.
- 프론트와 논의되지 않은 사항
- reCAPCHA 인증 결과를 어느정도로 해야할지 아직 정하지 않았습니다.
- Message라는 DTO를 사용했습니다. 초기에 작성된 터라 예외 상황을 Exception으로 처리하지 않았습니다.
- 작성자 juhyelee
- 이메일 인증 코드를 전송하는 API입니다.
- MemberController를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<Object> sendEmail(@Valid @RequestBody EmailiAddress address)
- MemberService를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public Message sendEmail(String email)
- 전송할 이메일을 구성하고, 해당 메일 주소로 전송합니다.
- 이때, 이메일 내용에는 7굴자의 랜덤한 문자열이 포함됩니다. 이는 아래와 같은 메서드에서 생성됩니다.
private String getAuthCode()
- 이때 생성된 코드는 EmailAuthService 필드에 저장이 됩니다.
- 그리고 구성된 내용들은 아래와 같은 메서드를 통해 전송됩니다.
private void send(EmailMessage emailMessage)
- 이때 사용되는 객체는 JavaMailSender로 HTML을 통해 메일 내용을 구성할 수 있습니다.
- 프론트와 논의된 사항
- 이메일 인증 코드는 유효기간이 있어야 합니다. 이는 Redis를 통해서 구현할 수 있을 것으로 보입니다.
- 이메일 인증 코드는 MemberService의 필드 내부가 아닌 Redis에 저장되어, 사용자가 인증 코드를 인증할 때 Redis를 탐색해서 인증하도록 해야할 것으로 보입니다.
- 없음
- 작성자 juhyelee
- 전송된 인증코드를 확인하는 API입니다.
- MemeberController를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<Object> authenticate(@RequestParam(name = "code") String code)
- EmailAuthService를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public void func()
- 사용자가 전달한 인증 코드를 인증합니다.
- EmailAuthService의 필드에 저장되어 있는 code를 불러와 동일한지 확인합니다.
- /membership/email 에서 설명한 것과 동일하게, Redis에 저장되어 있는 이메일 인증 코드를 조회해서 검사해야합니다.
- MemberController에서 매개변수를 RequestParam으로 설정되어 있습니다. Postman 테스트를 용이하게 하기 위해 그렇게 사용했던 것 같습니다.
- 로그인을 진행하고, access token과 refresh token을 발급합니다.
- 이때, refresh token은 Redis에 저장됩니다.
- SignController를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<Object> login(@Valid @RequestBody UserLoginRequest userLoginRequest)
- UserLoginRequest를 매개변수로 받습니다.
- JwtDto에 access token과 refresh token을 저장합니다.
- userId까지 프론트에서 받고싶다는 의견을 반영해 LinkedHashMap을 통해 Response body를 생성합니다.
- LoginService을 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public JwtDto login(String userEmail, String password)
- TokenProvider을 사용합니다.
- access token과 refresh token을 발급하고, refresh token을 redis에 저장합니다.
- TokenProvider을 사용합니다.
- Oauth2와 어떻게 합칠지 고민 중에 있었습니다.
- 로그인 실패 횟수를 체크해서 몇 회 이상 로그인시에, 다른 동작방식으로 진행되도록 해야합니다.
- 없음
- 작성자 junssong
- 로그아웃을 진행합니다.
- /logout 이렇게 했더니 login으로 리다이렉트 되는 문제 때문에 api명 변경 accessToken을 받아서 redis에 남은 만료시간까지 저장해서 추후에 다시 쓰이는일 방지
- SignController를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<?> logout(LogoutRequest logoutRequest, Authentication authentication)
- LogoutRequest에는 accessToken 하나가 들어있습니다.
- LoginService를 사용합니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<?> logout(LogoutRequest logoutRequest, Authentication authentication)
- 매개변수 중 Authentication는 인증 객체로, getName() 실행 시 userEmail을 반환합니다.
- 내부적으로 검증 이후, access, refresh 토큰을 발급받고 refresh를 redis에 저장합니다.
- 해당 로직에서 사용한 메서드는 다음과 같습니다.
tokenProvider.validateToken(logoutRequest.getAccessToken())
- access token을 검증합니다.
redisTemplate.opsForValue().get("refreshToken:" + authentication.getName())
- "refreshToken:" + email의 형식으로 redis에 refreshToken이 있는지 찾습니다.
- 만일, 있다면 삭제합니다.
tokenProvider.getExpiration(logoutRequest.getAccessToken()
- 만료시간을 검증합니다.
- 그리고 access, refresh 토큰을 발급받고 refresh를 redis에 저장
- 없음
- 인증되는 access token과 requestBody에 들어가는 accecc token이 다르면 이상한 예외가 튀어나옵니다
- 이는 UB라고 판단 중에 있습니다.
- 작성자 juhyelee
- access token이 만료되었을 때, 해당 API를 호출해서 새로운 access token을 발급합니다.
- SignController를 사용했습니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public ResponseEntity<?> reissueToken(@RequestBody ToReissueToken refreshToken)
- 클라이언트에서 받은 refresh token을 Base64로 디코드합니다.
- 디코드한 결과(JWT의 body에 해당하는 부분)를 토대로 Json객체를 생성합니다.
- 생성된 Json객체의 sub로 등록되어있는 User id 값을 찾아옵니다.
- 이때, sub에 해당하는 값이 없다면 예외처리 됩니다.
- LoginService를 사용했습니다.
- 해당 API를 수행하는 메서드는 아래와 같습니다.
public String reissue(Long userId, String refreshToken)
- User id를 통해 DB에 저장되어 있는 User를 조회합니다.
- 이는 refresh token에 대응되는 User가 있는지 확인하기 위함입니다.
- 동시에 refresh token을 조회할 때 사용될 email을 가져오기 위함입니다.
- Redis에 저장되어 있는 refresh token을 조회합니다.
- Redis에서 조회된 refresh token과 SungController에서 전달받은 refresh token이 동일한 토큰인지 확인합니다.
- 동일한 refresh token이 아니라면, 예외 처리합니다.
- 동일한 토큰이라면, access token을 발급해 Controller에 반환합니다.
- User id를 통해 DB에 저장되어 있는 User를 조회합니다.
- 없음
- Redis에서 조회한 refresh token이 NULL인지 확인한 후에, redirect를 해야할 것으로 보입니다.(저희의 생각)
builder.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
builder.addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class);
- addFilterBefore를 사용해서 필터를 거치기 이전에 인증을 진행합니다.
- 이때, 예외처리를 해주기 위해 ExceptionHandlerFilter클래스 사용합니다.
- 이때, 예외처리를 해주기 위해 ExceptionHandlerFilter클래스 사용합니다.
String token = tokenProvider.resolveAccessToken(request);
if (token == null) {
filterChain.doFilter(request, response);
return;
}
if (!tokenProvider.validateToken(token)) {
Authentication auth = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
return;
}
filterChain.doFilter(request, response);
- 헤더에서 토큰을 가져옵니다.
- 만일, 헤더에 토큰이 없다면 바로 filterChain.doFilter(request, response); 로 건너 뜁니다.
.and()
.addFilter(corsConfig.corsFilter())
.authorizeRequests()
.antMatchers("/login", "/membership/**", "/access-token").permitAll()
.anyRequest().authenticated()
- 그리고 건너뛰게 되면, corsFilter를 만나게 되어 허가 되지 않은 api들은 모두 거절 됩니다.
- 여기서 세세한 api 권한 설정이 더 필요합니다. (추가적인 세부 기획 필요합니다)
- 인증객체에 주입하여 api에서 인증객체를 불러올때 Principal 객체를 사용할 수 있게 됩니다.