OAuth2, API 형태로 만들기

Alex·2024년 10월 7일
0

시큐리티

목록 보기
2/2

JWT 방식에서 OAuth2 클라이언트 구성 시 고민점

로그인이 성공하면 JWT를 발급해야 한다.
프론트 단에서 로그인 경로에 대한 하이퍼링크를 실행하면 소셜 로그인창이 등장하고 로그인 로직이 수행된다.

로그인이 성공하면 JWT 가 발급되는데, JWT를 받을 로직이 없다.(리다이렉트를 쓰면 안됨)

잘못된 구현 방법

-프론트가 모든 책임을 맡음

소셜 로그인 버튼을 누르면, 외부 소셜 로그인 페이지가 뜬다. 코드-요청-API 클라이언트를 통해서 유저 정보 발급

편하지만, 프론트측에서 보낸 유저 정보의 진위 여부를 따져야 한다. 추가적인 보안 로직, 터널링을 해야 한다.

-프론트와 백이 책임을 나눔

로그인 요청->코드 발급->API CLIENT로 백엔드로 코드를 전달, 이걸 통해서 그 뒤의 작업을 한다.

(코드와 액세스토큰을 프론트에서 발급받고, 토큰을 백으로 보내서 그 뒤의 작업)

카카오 개발 포럼을 보면 코드나 액세스토큰을 전송하는 방법을 지양한다

-모든 책임을 백이 구현함

하이퍼링크로 백엔드 API GET요청을 보냄. 로그인 페이지에서 로그인후의 모든 작업을 백에서 함
다만, JWT를 획득하기가 까다롭다.

JWT 동작 원리

JWT 발급 및 검증

JWT

헤더: JWT 임을 명시, 사용된 암호 알고리즘

페이로드: 필요한 정보

시그니쳐:누가 발급했는지, 언제 발급했는지 등을 발급자를 확실하게 보여줌
(시그니쳐만 암호화가 진행됨)

(지폐와 같이 외부에서 그 금액을 확인하고 금방 외형을 따라서 만들 수 있지만 발급처에 대한 보장 및 검증은 확실하게 해야하는 경우에 사용한다. 따라서 토큰 내부에 비밀번호와 같은 값 입력 금지)

암호화 종류
양방향
대칭키 - 이 프로젝트는 양방향 대칭키 방식 사용 : HS256
비대칭키
단방향

이번에는 대칭키를 사용한다.


@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {

    private String role;
    private String name;
    private String userName;
}

@RequiredArgsConstructor
@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //OAuth2User
        CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();

        String username = customUserDetails.getUsername();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();
        String role = auth.getAuthority();

        String token = jwtUtil.createJwt(username, role, String.valueOf(60*60*60L));

        response.addCookie(createCookie("Authorization", token));
        response.sendRedirect("http://localhost:3000/");
    }

    private Cookie createCookie(String authorization, String token) {
        Cookie cookie = new Cookie(authorization, token);
        cookie.setMaxAge(60*60*60);

        //cookie.setSecure(true); https 아니니까
        cookie.setPath("/");//모든 전역에 설정
        cookie.setHttpOnly(true);

        return cookie;
    }


}

@RequiredArgsConstructor
@Service
public class CustomerOAuth2UserService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2User oauth2User = super.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        OAuth2Response oAuth2Response;

        if(registrationId.equals("naver")){
            oAuth2Response = new NaverResponse(oauth2User.getAttributes());

        } else if(registrationId.equals("google")){
            oAuth2Response = new GoogleResponse(oauth2User.getAttributes());

        } else {

            return null;
        }

        String userName = oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId();
        UserDto userDto = new UserDto("ROLE_USER", oAuth2Response.getName(), userName);


        return new CustomOAuth2User(userDto);
    }
}

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authorization = null;
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("Authorization")) {

                authorization = cookie.getValue();
            }
        }

        if (authorization == null) {

            filterChain.doFilter(request, response);
            //조건이 해당되면 메소드 종료 (필수)
            return;
        }
        String token = authorization;

        //토큰 소멸 시간 검증
        if (jwtUtil.isExpired(token)) {

            System.out.println("token expired");
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        //userDTO를 생성하여 값 set

        UserDto userDto = new UserDto();
        userDto.setRole(role);
        userDto.setUserName(username);


        //UserDetails에 회원 정보 객체 담기
        CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDto);

        //스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
        //세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }

}

@Component
public class JwtUtil {

    private SecretKey secretKey;

    public JwtUtil(@Value("${spring.jwt.secret}")String secret) {
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    public String createJwt(String username, String role, String expiredMs){
        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();

    }
}


import java.util.Collections;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomerOAuth2UserService customerOauth2UserService;

    private final CustomSuccessHandler customSuccessHandler;
    private final JwtUtil jwtUtil;

    public SecurityConfig(CustomerOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JwtUtil jwtUtil) {

        this.customerOauth2UserService = customOAuth2UserService;
        this.customSuccessHandler = customSuccessHandler;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {

                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

                        CorsConfiguration configuration = new CorsConfiguration();

                        configuration.setAllowedOrigins(Collections.singletonList("http://43.203.233.134:3000"));
                        configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowCredentials(true);
                        configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setMaxAge(3600L);

                        configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));

                        return configuration;
                    }
                }));

        //csrf disable
        http
                .csrf((auth) -> auth.disable());

        //From 로그인 방식 disable
        http
                .formLogin((auth) -> auth.disable());

        //HTTP Basic 인증 방식 disable
        http
                .httpBasic((auth) -> auth.disable());

        //JWTFilter 추가
        http
                .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

        //oauth2
        http
                .oauth2Login((oauth2) -> oauth2
                        .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
                                .userService(customerOauth2UserService))
                        .successHandler(customSuccessHandler)
                );
//경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated());

        //세션 설정 : STATELESS
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {


    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
                .exposedHeaders("Set-Cookie")
                .allowedOrigins("http://43.203.233.134:3000");
    }
}
profile
답을 찾기 위해서 노력하는 사람

0개의 댓글