Skip to content

06 ‐ 03 ‐ Security Wiki

Ryu(Paul) edited this page Nov 9, 2023 · 1 revision

INDEX

현재 Security에서 구현된 것

추후 진행해야 할 것

  • OAuth2로 소셜 로그인
  • 회원가입시, 비빌번호 2중체크 기능 추가
  • email, nickname 중복체크 API를 따로 제작
  • 프론트에서 AccessToken, RefreshToken 만료 체크
    • 이부분은 정확하게 논의 되지 않았습니다.
    • 프론트에서 만료 체크를 하는 이유는 로컬스토리지나 쿠키에 들어있는 token은 백엔드에서 확인이 불가능하며, 거기에 더해 여러번 핑퐁되기 때문입니다.
  • email로 전송하는 code를 redis에 저장
    • 유효시간을 구현하기 위함과, 여러 코드들을 저장해야하기 때문에 Redis를 사용하기로 했습니다. 다만, 프론트와는 아직 이야기가 되지 않았습니다.
  • 탈퇴기능
  • user, admin 등 securityConfig 권한 설정

API 설명

/membership

  • 회원가입을 진행하는 API입니다.

Controller

  • 작성자 juhyelee
  • MemberController를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
      public ResponseEntity<Object> signUp(@Valid @RequestBody UserInfo info)

Service

  • 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으로 처리하지 않았습니다.



/membership/email

  • 작성자 juhyelee
  • 이메일 인증 코드를 전송하는 API입니다.

Controller

  • MemberController를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public ResponseEntity<Object> sendEmail(@Valid @RequestBody EmailiAddress address)

Service

  • MemberService를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public Message sendEmail(String email)
    • 전송할 이메일을 구성하고, 해당 메일 주소로 전송합니다.
    • 이때, 이메일 내용에는 7굴자의 랜덤한 문자열이 포함됩니다. 이는 아래와 같은 메서드에서 생성됩니다.
      private String getAuthCode()
    • 이때 생성된 코드는 EmailAuthService 필드에 저장이 됩니다.
    • 그리고 구성된 내용들은 아래와 같은 메서드를 통해 전송됩니다.
      private void send(EmailMessage emailMessage)
    • 이때 사용되는 객체는 JavaMailSender로 HTML을 통해 메일 내용을 구성할 수 있습니다.

진행되어야할 사항

  • 프론트와 논의된 사항
    • 이메일 인증 코드는 유효기간이 있어야 합니다. 이는 Redis를 통해서 구현할 수 있을 것으로 보입니다.
    • 이메일 인증 코드는 MemberService의 필드 내부가 아닌 Redis에 저장되어, 사용자가 인증 코드를 인증할 때 Redis를 탐색해서 인증하도록 해야할 것으로 보입니다.

참고사항

  • 없음



/membership/email/code

  • 작성자 juhyelee
  • 전송된 인증코드를 확인하는 API입니다.

Controller

  • MemeberController를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public ResponseEntity<Object> authenticate(@RequestParam(name = "code") String code)

Service

  • EmailAuthService를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public void func()
    • 사용자가 전달한 인증 코드를 인증합니다.
    • EmailAuthService의 필드에 저장되어 있는 code를 불러와 동일한지 확인합니다.

진행되어야할 사항

  • /membership/email 에서 설명한 것과 동일하게, Redis에 저장되어 있는 이메일 인증 코드를 조회해서 검사해야합니다.

참고사항

  • MemberController에서 매개변수를 RequestParam으로 설정되어 있습니다. Postman 테스트를 용이하게 하기 위해 그렇게 사용했던 것 같습니다.



/login

  • 로그인을 진행하고, access token과 refresh token을 발급합니다.
    • 이때, refresh token은 Redis에 저장됩니다.

Controller

  • SignController를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public ResponseEntity<Object> login(@Valid @RequestBody UserLoginRequest userLoginRequest)
    • UserLoginRequest를 매개변수로 받습니다.
    • JwtDto에 access token과 refresh token을 저장합니다.
    • userId까지 프론트에서 받고싶다는 의견을 반영해 LinkedHashMap을 통해 Response body를 생성합니다.

Service

  • LoginService을 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public JwtDto login(String userEmail, String password)
    • TokenProvider을 사용합니다.
      • access token과 refresh token을 발급하고, refresh token을 redis에 저장합니다.

진행되어야할 사항

  • Oauth2와 어떻게 합칠지 고민 중에 있었습니다.
  • 로그인 실패 횟수를 체크해서 몇 회 이상 로그인시에, 다른 동작방식으로 진행되도록 해야합니다.

참고사항

  • 없음



/user-logout

  • 작성자 junssong
  • 로그아웃을 진행합니다.
  • /logout 이렇게 했더니 login으로 리다이렉트 되는 문제 때문에 api명 변경 accessToken을 받아서 redis에 남은 만료시간까지 저장해서 추후에 다시 쓰이는일 방지

Controller

  • SignController를 사용합니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public ResponseEntity<?> logout(LogoutRequest logoutRequest, Authentication authentication)
    • LogoutRequest에는 accessToken 하나가 들어있습니다.

Service

  • 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라고 판단 중에 있습니다.



/access-token

  • 작성자 juhyelee
  • access token이 만료되었을 때, 해당 API를 호출해서 새로운 access token을 발급합니다.

Controller

  • SignController를 사용했습니다.
  • 해당 API를 수행하는 메서드는 아래와 같습니다.
    public ResponseEntity<?> reissueToken(@RequestBody ToReissueToken refreshToken)
    • 클라이언트에서 받은 refresh token을 Base64로 디코드합니다.
    • 디코드한 결과(JWT의 body에 해당하는 부분)를 토대로 Json객체를 생성합니다.
    • 생성된 Json객체의 sub로 등록되어있는 User id 값을 찾아옵니다.
      • 이때, sub에 해당하는 값이 없다면 예외처리 됩니다.

Service

  • 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에 반환합니다.

진행되어야할 사항

  • 없음

참고사항

  • Redis에서 조회한 refresh token이 NULL인지 확인한 후에, redirect를 해야할 것으로 보입니다.(저희의 생각)



SecuriyChain 인증 과정 설명

SecurityConfig

builder.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
builder.addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class);
  • addFilterBefore를 사용해서 필터를 거치기 이전에 인증을 진행합니다.
    • 이때, 예외처리를 해주기 위해 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 객체를 사용할 수 있게 됩니다.
Clone this wiki locally