Spring Security 이용해서 SNS 로그인 Rest API 환경으로 구현하기

Jaeyoung·2023년 3월 12일
1
post-thumbnail
post-custom-banner

이번에 졸업프로젝트를 Android앱으로 준비하면서 서버간 통신을 위해 Rest Api 환경으로 구현하게 되면서 Sns 로그인을 처리하는 과정을 정리하기 위해 작성하게 되었습니다. 과정을 하나씩 보면서 로그인 관련 플로우를 이해하면서 스프링 시큐리티 인증처리가 어떻게 되는지도 쉽게 이해할 수 있도록 해보겠습니다.

Client Login Flow

일단 클라이언트에서 로그인 과정이 어떻게 되는지 먼저 알아보도록 하겠습니다. 일반적으로 권장하는 방법은 클라이언트에서 AccessToken을 받는게 아닌 Authorization Code를 리소스 서버에서 받아 서버로 해당 코드를 전달하고 서버에서 클라이언트에서 받은 Authorization Code와 Client Id,Client Secrete를 통해 리소스 서버에 요청을 보낸 후 AccessToken을 받아 클라이언트에서 AccessToken를 받는 것보다 보안적으로 더 좋기 때문에 권장하는 방법인 것 같습니다. 하지만 Code로 처리하게되면 많이 귀찮아지기 때문에 클라이언트에서 Code로 받지 않고 저는 AccessToken을 받아 처리하도록 하겠습니다.

Spring Security Authentication Filter

Spring Security는 인증을 하기 위한 Filter를 설정할 수 있는데 해당 인증 필터를 만들어 보도록 하겠습니다. 대표적으로 많이사용하는 정의된 인증 필터는 UsenamerPasswordAuthenticationFilter입니다. 해당 필터는 AbstractAuthenticationProcessingFilter를 상속받고 있는데 저도 해당 필터를 상속 받아서 처리 하도록 하겠습니다. 해당 필터를 상속받는 이유는 인증관련 로직만 작성하면 나머지 작업 예를 들면 인증 성공, 인증 실패에 대한 처리를 부모 객체가 처리해 주기 때문에 사용하게 되었습니다. AbstractAuthenticationProcessingFilter의 dofilter 코드를 보면서 왜 쓰는지 알아보도록 하겠습니다.

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}

맨 위에 requiresAuthentication를 통해 등록된 RequestMatcher로 그냥 처리해줘야하는 URL인지 아닌지 확인해서 아니라면 dofilter를 진행해서 다음 필터가 인증을 처리할 수 있도록 Early return으로 처리가 되고 인증처리를 하는 abstarct 메서드인 attemptAuthentication를 호출해서 Authentication를 받아서 null이라면 return으로 필터를 종료하게 됩니다. 그리고 AuthenticationException이나 InternalAuthenticationServiceException이 발생하지 않는다면 sucessfulAuthentication을 호출하고 Exception이 발생한다면 unsuccessfulAuthentication을 실행합니다. successfulAuthentication 로직 부터 보자면 아래와 같습니다.

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
		Authentication authResult) throws IOException, ServletException {
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		context.setAuthentication(authResult);
		this.securityContextHolderStrategy.setContext(context);
		this.securityContextRepository.saveContext(context, request, response);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
		}
		this.rememberMeServices.loginSuccess(request, response, authResult);
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

SecurityContext에 Authentication을 저장해주고 successHandler를 호출해 주게 됩니다. 아래는 실패했을 때 호출되는 메서드입니다.

 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException failed) throws IOException, ServletException {
		this.securityContextHolderStrategy.clearContext();
		this.logger.trace("Failed to process authentication request", failed);
		this.logger.trace("Cleared SecurityContextHolder");
		this.logger.trace("Handling authentication failure");
		this.rememberMeServices.loginFail(request, response);
		this.failureHandler.onAuthenticationFailure(request, response, failed);
}

SecurityContext를 초기화 해주고 failureHandler가 호출됩니다.

네 이렇게 왜 사용해야하는지 알아 봤구요 이걸 기반으로 소셜 로그인에 대한 인증처리를 위한 Oauth2AuthenticationFilter를 만들어 보겠습니다.

Oauth2AuthenticationFilter 구현

일단 필터를 생성하기 전에 필요한 것들이 있는데요 시큐리티 필터의 인증 수행 방식을 정의하는 AuthenticationManager가 있는데 일반적으로 많이 쓰이는 구현체는 ProviderManager입니다. ProviderManager는 동작을 AuthenticationProvider List에 위임하게 되는데요 List를 순회하면서 인증처리를 할 수 있는지 확인하고 인증을 진행하게 됩니다. Oauth2AuthenticationFilter 인증 필터를 생성해 보도록 하겠습니다.

@Slf4j
public class Oauth2AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private static final String MATCH_URL_PREFIX = "/login/oauth2/";
    private static final String ACCESS_TOKEN_HEADER = "Authorization";
    private static final String tokenType = "Bearer ";

    public Oauth2AuthenticationFilter(AuthenticationSuccessHandler authenticationSuccessHandler,
                                      AuthenticationProvider... authenticationProvider) {
        super(new AntPathRequestMatcher(MATCH_URL_PREFIX + "*"));
        this.setAuthenticationManager(new ProviderManager(authenticationProvider));
        this.setAuthenticationSuccessHandler(authenticationSuccessHandler);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        OauthServerType serverType = extractOauthServerType(request);
        String accessToken = extractToken(request);
        return getAuthenticationManager().authenticate(new Oauth2AuthenticationToken(accessToken, serverType));
    }

    public String extractToken(HttpServletRequest request){
        String header = request.getHeader(ACCESS_TOKEN_HEADER);
        if(!header.startsWith(tokenType))
            throw new AuthenticationServiceException("토큰이 존재하지 않습니다.");
        return header.substring(tokenType.length());
    }

    public OauthServerType extractOauthServerType(HttpServletRequest request) {
        String extractType = request.getRequestURI().substring(MATCH_URL_PREFIX.length());
        return Arrays.stream(OauthServerType.values())
                .filter(type -> type.name().toLowerCase().equals(extractType))
                .findFirst()
                .orElseThrow(() -> new AuthenticationServiceException("잘못된 요청 입니다."));
    }
}

생성자 부터 보자면 AuthenticationSuccessHandler, AuthenticationProvider를 인자로 받고 있습니다. 그리고 AntPathRequestMatcher를 등록해서 Match되는 Url만 처리할 수 있도록 해줍니다. 그리고 AuthenticationManager를 등록해줍니다. 여기서 중요한 부분은 attemptAuthentication 메서드 입니다. 먼저 uri에서 oauthServerType를 추출합니다. 그리고 Header에서 토큰이 있는지 검증하고 토큰을 꺼내옵니다. 이러한 데이터를 바탕으로 Oauth2AuthenticationToken이라는 생성해줍니다. Oauth2AuthenticationToken은 AbstractAuthenticationToken을 상속한 객체입니다. AbstractAuthenticationToken는 Authentication인터페이스를 구현하고 있습니다. 그래서 Oauth2AuthenticationToken를 보자면 아래와 같습니다.

@Getter
@ToString
public class Oauth2AuthenticationToken extends AbstractAuthenticationToken {
    private String accessToken;
    private OauthServerType type;

    public Oauth2AuthenticationToken(String accessToken, OauthServerType type){
        super(null);
        this.accessToken = accessToken;
        this.type = type;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

생성자로 accessToken과 OauthServerType를 받고 있습니다. 다음으로는 인증을 수행하는 객체인Oauth2AccessTokenAuthenticationProvider를 생성해 주도록 하겠습니다.

@RequiredArgsConstructor
@Component
public class Oauth2AccessTokenAuthenticationProvider implements AuthenticationProvider {
    private final OauthTokenResolverFactory factory;
    private final MemberService memberService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Oauth2AuthenticationToken token = (Oauth2AuthenticationToken) authentication;
        OauthTokenResolver resolver = factory.getResolver(token.getType());
        OauthInfo oauthInfo = resolver.resolve(token.getAccessToken());
        MemberDto memberDto = memberService.socialLogin(token.getType(), oauthInfo);
        List<GrantedAuthority> grantedAuthorityList = getAuthorities(memberDto);
        return new UsernamePasswordAuthenticationToken(memberDto.getEmail(),null,grantedAuthorityList);
    }

    private List<GrantedAuthority> getAuthorities(MemberDto memberDto){
        List<GrantedAuthority> result = new ArrayList<>();
        MemberRole role = memberDto.getRole();
        result.add(new SimpleGrantedAuthority(role.name()));
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return Oauth2AuthenticationToken.class.isAssignableFrom(authentication);
    }
}

Oauth2AccessTokenAuthenticationProvider의 authenticate 메서드를 보면 아까 생성했던 Oauth2AuthenticationToken으로 캐스팅 한 후 OauthTokenResolverFactory를 통해 OauthTokenResolver를 받아와서 OauthInfo를 받아옵니다. OauthTokenResolverFactory와 OauthInfo에 대해 작성해보도록 하겠습니다.

@Slf4j
@Component
public class OauthTokenResolverFactory {
    private final List<OauthTokenResolver> resolvers = new ArrayList<>();

    public OauthTokenResolverFactory(OauthTokenResolver... googleOauthTokenResolver) {
        resolvers.addAll(List.of(googleOauthTokenResolver));
    }

    public OauthTokenResolver getResolver(OauthServerType type) {
        return resolvers.stream()
                .filter(r -> r.supports(type))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Not supports OauthTokenResolver"));
    }
}

생성자로 OauthTokenResolver 리스트를 받아와서 List로 변환시켜서 resolvers에 저장합니다 그리고 getResolver의 인자로 OauthServerType를 받고 resolvers를 순회하면서 해당 Type를 처리할 수 있는지 확인하고 처리할 수 있는게 없다면 Exception을 발생시키고 처리할 수 있다면 해당 Resolver를 반환해줍니다.

@Getter
@AllArgsConstructor
public class OauthInfo {
    private String id;
    private String email;
}
public interface OauthTokenResolver {
    OauthInfo resolve(String token);
    boolean supports(OauthServerType type);
}

OauthTokenResolver는 Token을 통해 OauthInfo를 가져오는 interface입니다. supports 메서드는 해당 Token을 처리할 수 있는지 확인하는 메서드입니다. 제가 이용할 플랫폼은 Naver와 Google이기 때문에GoogleOauthTokenResovler와 NaverGoogleOauthTokenResovler를 생성해주도록 하겠습니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class GoogleOauthTokenResolver implements OauthTokenResolver{
    private final RestTemplate restTemplate;

    @Override
    public OauthInfo resolve(String token) {
        URI uri = UriComponentsBuilder
                .fromUriString("https://oauth2.googleapis.com/tokeninfo")
                .queryParam("id_token", token)
                .encode()
                .build()
                .toUri();
        ResponseEntity<Map> response = restTemplate.getForEntity(uri, Map.class);
        Map<String, Object> post = response.getBody();
        String email = (String) post.get("email");
        String sub = (String) post.get("sub");
        return new OauthInfo(sub,email);
    }

    @Override
    public boolean supports(OauthServerType type) {
        return type.equals(OauthServerType.GOOGLE);
    }
}
@Slf4j
@Component
@RequiredArgsConstructor
public class NaverOauthTokenResolver implements OauthTokenResolver {
    private final RestTemplate restTemplate;

    @Override
    public OauthInfo resolve(String token) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer "+token);
        HttpEntity request = new HttpEntity(headers);
        ResponseEntity<Map> response = restTemplate.exchange("https://openapi.naver.com/v1/nid/me", HttpMethod.GET, request, Map.class);
        Map<String, Object> post = response.getBody();
        Map<String,Object> result = (Map<String,Object>) post.get("response");
        String email = (String) result.get("email");
        String id = (String) result.get("id");
        return new OauthInfo(id, email);
    }

    @Override
    public boolean supports(OauthServerType type) {
        return type.equals(OauthServerType.NAVER);
    }
}

그리고 MemberService를 통해 login처리를 해줍니다. 일단 회원가입과 로그인을 하기위한 Memeber Entity와 MemberRespotiroy MemberService를 생성해주도록 하겠습니다.

@Getter
@NoArgsConstructor
@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    @Column(unique = true,nullable = false)
    private String email;

    @Column(unique = true)
    private String nickname;

    @Enumerated(value = EnumType.STRING)
    private OauthServerType oauthServerType;

    private String oauthId;

    private String profileImage = "";

    @Enumerated(value = EnumType.STRING)
    private MemberRole role;

    public Member(OauthServerType type , OauthInfo info){
        this.oauthServerType = type;
        this.email = info.getEmail();
        this.oauthId = info.getId();
        this.role = MemberRole.USER;
    }
}
public enum MemberRole {
    USER,NEW_USER,COUNSELOR
}
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}
@RequiredArgsConstructor
@Transactional
@Service
public class MemberService {
    private final MemberRepository repository;

    public MemberDto socialLogin(OauthServerType type, OauthInfo info){
        Member member = repository.findByEmail(info.getEmail())
                .orElseGet(() -> repository.save(new Member(type, info)));
        return new MemberDto(member);
    }
}

MemberService를 보면 만약 email를 통해 Member를 검색하고 없다면은 회원가입 대상이기 때문에 type과 info로 Member 엔티티를 생성하고 저장해주도록 합니다. 그리고 최종적으로 MemberDto를 반환하게 됩니다. MemberDto는 아래와 같습니다.

@Getter
@NoArgsConstructor
@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    @Column(unique = true,nullable = false)
    private String email;

    @Column(unique = true)
    private String nickname;

    @Enumerated(value = EnumType.STRING)
    private OauthServerType oauthServerType;

    private String oauthId;

    private String profileImage = "";

    @Enumerated(value = EnumType.STRING)
    private MemberRole role;

    public Member(OauthServerType type , OauthInfo info){
        this.oauthServerType = type;
        this.email = info.getEmail();
        this.oauthId = info.getId();
        this.role = MemberRole.NEW_USER;
    }
}

그래서 다시 Oauth2AccessTokenAuthenticationProvider로 돌아와서 login 처리를 해주고 받아온 MemberDto로 UsernamePasswordAuthenticationToken으로 생성해서 반환해줍니다. 이렇게 되면 최종적으로 인증이 완료가 되고 AuthenticationSuccessHandler를 호출해주게 됩니다.

AuthenticationJwtReturnHandler

AuthenticationJwtReturnHandler 같은 경우에는 인증이 완료되었을 때 호출이 되는데요 여기서 Jwt Token을 생성해서 Response로 넘겨주는 작업을 해줘야하는데 공통적인 로직이 많이 발생할 것 같으니 일단 Response를 보내는 로직을 HttpUtils라는 클래스로 만들어보도록 하겠습니다. HttpUtils는 아래와 같습니다.

@RequiredArgsConstructor
@Component
public class HttpUtils {
    private final ObjectMapper objectMapper;
    public <T> void sendResponse(HttpServletResponse response, int code , String message, T data) throws IOException {
        HttpResponse<T> httpResponse = new HttpResponse<>(code,message,data);
        String json = objectMapper.writeValueAsString(httpResponse);
        response.setStatus(code);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(json);
    }
}

일단 보시다시피 HttpResponse라는 클래스를 반환해주고있는데요 HttpResponse는 일관적인 Response를 전달해주기 위해 만든 클래스입니다. 단순히 Http Status Code, Server Message, Data 이렇게 구성되어있습니다.

@Getter
@AllArgsConstructor
@EqualsAndHashCode
public class HttpResponse<T> {
    private Integer code;
    private String message;
    private T data;
}

그래서 sendResponse 메소드의 인자로 받은 code와 메세지 data를 통해 HttpResponse를 생성해주고 이것을 ObjectMapper를 통해서 Json형태로 변환해줍니다. 그다음에 HttpServletResponse에 code와 encoding contentType를 설정해줍니다. 최종적으로 Respones를 보내주게 됩니다. 다시 돌아와서 우리는 Jwt를 만들어서 내려줄것이기 때문에 Jwt를 생성하고 처리하는 클래스를 만들어볼텐데 중요한 부분이 아니기 때문에 이 부분은 설명은 생략하고 코드만 남겨두겠습니다.

@Component
public class JwtProvider {
    public final byte[] secretByte;
    private final Key key;

    public JwtProvider(@Value("${jwt.secret}") String secret) {
        secretByte = secret.getBytes();
        key = Keys.hmacShaKeyFor(secretByte);
    }

    public static final String authKey = "auth";

    public Jwt createJwt(Map<String, Object> claims) {
        String accessToken = createToken(claims, getExpireDateAccessToken());
        String refreshToken = createToken(claims, getExpireDateRefreshToken());
        return Jwt.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    public String createToken(Map<String, Object> claims, Date expireDate) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(expireDate)
                .signWith(key)
                .compact();
    }

    public Claims getClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public Date getExpireDateAccessToken() {
        long expireTimeMils = 1000 * 60 * 60;
        return new Date(System.currentTimeMillis() + expireTimeMils);
    }

    public Date getExpireDateRefreshToken() {
        long expireTimeMils = 1000L * 60 * 60 * 24 * 60;
        return new Date(System.currentTimeMillis() + expireTimeMils);
    }
}
@Getter
@ToString
public class Jwt {
    private String accessToken;
    private String refreshToken;

    @Builder
    public Jwt(String accessToken, String refreshToken){
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

본격적으로 AuthenticationJwtReturnHandler코드를 분석 해보겠습니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationJwtReturnHandler implements AuthenticationSuccessHandler {
    private final ObjectMapper mapper;
    private final JwtProvider jwtProvider;
    private final HttpUtils httpUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("로그인 성공 : {}", authentication);
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        JwtClaimsInfo jwtClaimsInfo = new JwtClaimsInfo(token);
        String json = mapper.writeValueAsString(jwtClaimsInfo);
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtProvider.authKey, json);
        Jwt jwt = jwtProvider.createJwt(claims);
        httpUtils.sendResponse(response, HttpServletResponse.SC_OK, "토큰 발급", jwt);
    }
}

onAuthenticationSuccess 메소드는 AuthenticationSuccessHandler를 구현한 클래스로 인증이 성공했을 때 호출이 되는데요 인자로 Authentication를 받고있어 이전에 인증했던 인증정보를 가져올 수 있습니다. 이전에 인증정보를 UsernamePasswordAuthenticationToken으로 저장했기 때문에 해당 객체로 캐스팅 해줍니다. 그리고 Jwt에 저장할 정보인 JwtClaimsInfo를 생성해줍니다. JwtClaimsInfo는 아래와 같습니다.

@Getter
@AllArgsConstructor
public class JwtClaimsInfo {
    private String email;
    private List<String> authorities;

    public JwtClaimsInfo(UsernamePasswordAuthenticationToken token) {
        List<String> grantAuthority = token.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList();
        this.email = (String) token.getPrincipal();
        this.authorities = grantAuthority;
    }
}

GrantedAuthority를 String으로 변환시켜준 이유는 GrantedAuthority는 interface이기 때문에 ObjectMapper에서 생성해줄 수 없기 때문입니다. 그래서 AuthenticationJwtReturnHandler를 다시 보자면 JwtClaimsInfo를 Json으로 변환하고 claims으로 생성해 줍니다. 그리고 Jwt를 생성해주고 Jwt를 Response로 전송해줍니다.

JwtAuthenticationFilter

이제 마지막으로 Jwt를 인증하는 JwtAuthenticationFilter필터를 만들어 보도록 하겠습니다.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private static final String ACCESS_TOKEN_HEADER = "Authorization";
    private static final String tokenType = "Bearer ";
    private final JwtProvider jwtProvider;
    private final HttpUtils httpUtils;
    private final MemberService memberService;

    private final ObjectMapper mapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
        String accessToken = extractToken(request);
        try {
            Claims claims = jwtProvider.getClaims(accessToken);
            String json = claims.get(JwtProvider.authKey, String.class);
            JwtClaimsInfo claimsInfo = mapper.readValue(json, JwtClaimsInfo.class);
            String email = claimsInfo.getEmail();
            MemberDto member = memberService.findMemberByEmail(email);
            if (member == null)
                throw new AuthenticationServiceException("사용자가 없음");
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(email, null,
                            claimsInfo.getAuthorities().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())));
            request.setAttribute("member",member);
            doFilter(request, response, filterChain);
        } catch (ExpiredJwtException e) {
            httpUtils.sendResponse(response, HttpServletResponse.SC_FORBIDDEN, "토큰 만료", null);
        } catch (Exception e) {
            log.error("ErrorMessage : {}", e.getMessage());
            httpUtils.sendResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "인증 실패", null);
        }
    }

    public String extractToken(HttpServletRequest request) {
        String header = request.getHeader(ACCESS_TOKEN_HEADER);
        if (!header.startsWith(tokenType))
            throw new AuthenticationServiceException("토큰이 존재하지 않습니다.");
        return header.substring(tokenType.length());
    }
}

OncePerRequestFilter를 상속받았는데 요청당 딱 한번만 처리하도록 해당 필터를 사용하게 되었습니다. doFilter 메소드를 보면 처음에 Header에서 토큰을 가져옵니다 만약 토큰이 없다면 AuthenticationException을 발생시킵니다. 그 다음에는 JwtProvider를 통해 Jwt를 Parsing해서 claims를 가져오게 됩니다. 이러한 과정에서 유효한 토큰이 아니거나 토큰이 만료된 경우에는 Exception을 발생시킵니다. 정상적으로 가져왔다면 Claims에서 JwtClaimsInfo를 꺼내온다음 Email으로 사용자 조회를 합니다. 만약 사용자가 없다면 AuthenticationException을 발생시키고 정상적으로 사용자를 가져왔다면 SecurityContextHolder에 저장해주도록 합시다. 그리고 HttpServletRequest에 attribute로 MemberDto를 설정해주고 다음 필터를 수행시킵니다.

마지막으로

이렇게 여러가지 필터들을 등록해서 SNS로그인 처리를 해주게 되었는데요 여기서 빠진 중요한 내용이 있는데 Jwt에서 RefreshToken를 갱신하는 부분이 빠졌습니다. 다음에 해당 내용으로 따로 포스팅 하도록 하겠습니다. 아무래도 처음 스프링 시큐리티를 적용하면서 코드는 그렇게 길진 않지만 해당 기능을 생각하고 구현하는데 시간을 많이 썼던 것 같습니다. 하지만 직접 생각하고 구현하면서 실력도 향상이 된 느낌이고 Spring Security에 대해 한발짝 더 다가가게 된 것 같습니다. 다른 블로그 글들을 참고해서 그대로 구현하는 것도 좋지만 직접 생각하고 직접 구현해보는게 더 좋은 것 같다는 생각이 들었습니다.

profile
Programmer
post-custom-banner

0개의 댓글