[Spring Security] 기존의 일반/소셜 로그인 로직의 리팩토링을 결심한 이유

김태훈·2023년 10월 28일
0

Spring Security

목록 보기
4/7
post-thumbnail

커밋 기록상, 8월 18일에 마무리 되었던 Spring Security 로직이 대폭 수정되었다.
중간에 한번 로그인 관련 로직을 대폭 변경하였고, 이번주에 프로젝트에 유저별 Role에 따른 접근 제한이 필요하게 된 상황이라 또 다시 Security Code를 손봐야 했다.

이전에는, 유저의 Role이 필요가 없는 상황이라서, Authentication 객체 내에 존재하는 Authorities들을 Empty List로 반환해서 모든 로직을 한꺼번에 처리하였다.
하지만 이제는 진짜 Authority가 필요한 상태이다.

그래서 이번 포스트에는 두번에 거친 리팩토링 과정을 정리할 예정이다.
1. 일반 로그인을 위해 Controller에서 Authentication 직접 만들었던 로직을 Security Filter를 온전히 이용하게 리팩토링
2. 유저의 Role을 추가하기 위해 거친 리팩토링 과정

1. 기존의 코드와 리팩토링을 시행한 이유

기존 포스트를 보면 어떤식으로 코드를 전개했는지 살펴 볼 수 있다.
https://velog.io/@goat_hoon/Spring-Security를-활용한-JWT-도입기

원래는 JWT관련 Bearer token 정보를 Http Header에 넣고 인증 과정을 거쳤다.
하지만 Next js 서버 사이드 렌더링의 과정에서 Next 서버가 Http Message에 존재하는 header정보를 못 가져오는 문제가 발생했다. 13버전으로 upgrade되면서 발생한 문제라고 하는데, 2023년 7월 기준으로는 별다른 방법을 찾지 못했다.
그래서 Cookie를 이용하는 방식으로 대폭 수정하였다.
만일, 굳이 SSR을 사용하지 않는다면 Http Header 방식을 사용하면 될 것이다.

1) AuthController

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class AuthController {
    private final AuthService authService;
    @Value("${jwt.domain}") String domain;

    @ResponseBody
    @PostMapping("/login")
    public ResponseEntity<TokenDto> login(@Valid @RequestBody NonSocialMemberLoginDto nonSocialMemberLoginDto) {
        TokenDto tokenDto = authService.makeTokenInfo(nonSocialMemberLoginDto);
        HttpHeaders httpHeaders = new HttpHeaders();
        //SSL 미설정으로 인한 Secure 옵션 미설정
        httpHeaders.add("Set-Cookie", "token="+tokenDto.getToken()+"; "+"Path=/; "+"Domain="+domain+"; "+"HttpOnly; "+"Max-Age=1800; SameSite=None; ");
        httpHeaders.add("Access-Control-Allow-Credentials","true");
        return new ResponseEntity<>(tokenDto, httpHeaders, HttpStatus.OK);
    }
}

2) AuthService

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService implements UserDetailsService {
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final Map<String,MemberRepository> repositoryMap;

    public TokenDto makeTokenInfo(NonSocialMemberLoginDto nonSocialMemberLoginDto){
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(nonSocialMemberLoginDto.getUserEmail(), nonSocialMemberLoginDto.getUserPw());
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = tokenProvider.createToken(authentication);
        return new TokenDto(jwt,Long.parseLong(authentication.getName()));
    }
    @Override
    public UserDetails loadUserByUsername(String userEmail) throws UsernameNotFoundException {
        MemberRepository memberRepository = repositoryMap.get("nonSocialMemberRepository");
        Optional<NonSocialMember> nonSocialMember = memberRepository.findByEmail(userEmail);
        if (nonSocialMember.isPresent()) {
            NonSocialMember member = nonSocialMember.get();
            log.info("member info in loadByUsername method = {}", member.getAuthId());
            //non social, social 섞어있기 때문에, user_id를 CustomUserDetail 의 id로 생성합니다. ->토큰의 getName의 user_id가 들어갑니다.
            return new CustomUserDetails(member.getUserId(),member.getUserEmail(),member.getUserPw(),true,false );
        } else {
            throw new UsernameNotFoundException("User not found with userEmail: " + userEmail);
        }
    }
}
  1. 로그인 api 경로로 유저 Email과 Password가 담긴 요청이 들어오면 UsernamePasswordAuthenticationToken을 직접 만들어낸다.

  2. authenticationManangerBuilder.getObject().authenticate()메서드를 활용해서 UserDetailsService를 구현하고 있는 AuthServiceloadUserByUsername 을 실행하여 DB내의 사용자 정보를 CustomUserDetails에 담는다.
    아래 사진은 DaoAuthenticationProvider (authenticate 메서드를 실행하는 AuthenticationProvider 의 실질적 구현체)가 DB 내에 존재하는 user를 찾고, UserDetails를 반환하는 코드이다.

  3. UserDetails 정보와 유저가 입력한 정보가 담긴UsernamePasswordAuthenticationToken 를 비교하여 일치하는지 확인한다.
    인증이 성공하면, 인증이 성공한 UsernamePasswordAuthenticationToken 를 반환한다.

3) 그래서 바꾼 이유는?

Spring Security 아키텍쳐를 무시하고 만든 느낌이었기 때문이다. Spring 아키텍쳐도, 특히 Security Architecture를 잘 몰랐을 때, 무작정 구글링해서 다른사람이 작성한 글을 보고 감을 잡으면서 코드를 만들었기 때문이다.
물론 뒤늦게 알아본 정보이지만,
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#store-authentication-manually
manual 하게 Spring Security Filter방식을 의존하지 않고 작성하는 코드로 소개한 것과 거의 동일했다.

그래서 조금 찝찝했었다. Spring Security가 잘 만들어 놓은 Filter를 놔두고 manually 하게 authenticate를 해야할 필요가 있을까..?

profile
기록하고, 공유합시다

0개의 댓글