(Spring) sso 외부 인증 토큰을 검증하고 세션에 사용자 정보를 저장하는 흐름

2026. 1. 7. 15:32·개발/spring boot
728x90
반응형

 

외부 시스템(SSO, 인증 서버, 쿠폰 인증 서버 등)과 연동할 때 흔히 나오는 패턴이 있습니다.

  1. 외부 서버가 인증 결과(토큰) 를 우리 서버에 보내줌
  2. 우리 서버는 그 토큰을 외부 서버에 다시 물어봐서 검증함
  3. 검증 성공 시, 사용자 정보를 세션(HttpSession) 에 저장
  4. 이후 화면/다음 요청에서 세션에서 꺼내 사용

이 글에서는 “쿠폰 인증 서버” 예시로 설명하겠습니다. (SSO와 원리는 동일합니다)


1. 전체 흐름 한 장 요약

  • 사용자가 쿠폰 인증을 시도
  • 외부 쿠폰 서버가 우리 서버의 /coupon/callback 으로 token, txId 등을 POST
  • 우리 서버는 쿠폰 서버의 /api/verify로 서버-서버 통신하여 토큰 검증
  • 성공하면 세션에 VerifiedUser 저장
  • redirect:/coupon/result로 이동
  • 결과 페이지에서 세션 정보를 읽어서 보여줌

2. 왜 “서버가 다시 검증”해야 할까?

브라우저가 보내온 token을 그대로 믿으면 위험합니다.

  • 누군가가 token 값을 조작해서 보낼 수 있음
  • “진짜 쿠폰 서버가 발급한 토큰인지” 확인이 필요함

그래서 우리 서버가 쿠폰 서버에 직접 요청해서 검증하는 방식이 안전합니다.
(SSO도 똑같습니다)


3. 예시 코드 (완전히 새로운 코드)

목적: 외부에서 받은 token을 검증하고, 성공 시 세션에 사용자 정보를 저장한 뒤 리다이렉트
✅ 실제 프로젝트 코드/명칭과 무관한 “교육용 예시”입니다.

@Controller
@RequestMapping("/coupon")
public class CouponAuthController {

    private final CouponVerifyClient couponVerifyClient;

    public CouponAuthController(CouponVerifyClient couponVerifyClient) {
        this.couponVerifyClient = couponVerifyClient;
    }

    // 1) 외부 서버가 호출하는 콜백 엔드포인트
    @PostMapping("/callback")
    public String couponCallback(@ModelAttribute CouponCallbackForm form,
                                 HttpServletRequest request,
                                 RedirectAttributes redirectAttributes) {

        String token = form.getToken();   // 외부 서버가 준 인증 토큰
        String txId  = form.getTxId();    // 외부 서버가 준 트랜잭션 ID

        // 1차 방어: 형식/존재 체크
        if (token == null || token.isBlank()) {
            redirectAttributes.addFlashAttribute("message", "인증 토큰이 없습니다.");
            return "redirect:/coupon/result";
        }

        // 2차 검증: 서버-서버 통신으로 토큰 진위 확인
        CouponVerifyResponse verify = couponVerifyClient.verify(token, txId);

        HttpSession session = request.getSession();

        if (verify.isSuccess()) {
            // ✅ 검증 성공: 세션에 사용자 정보(또는 인증 결과)를 저장
            VerifiedUser user = new VerifiedUser(
                    verify.getUserId(),
                    verify.getUserName(),
                    verify.getGrade()
            );

            session.setAttribute("VERIFIED_USER", user);
            session.setAttribute("VERIFY_STATUS", "SUCCESS");

        } else {
            // ❌ 실패: 실패 상태 저장(메시지는 과도한 정보 노출 주의)
            session.removeAttribute("VERIFIED_USER");
            session.setAttribute("VERIFY_STATUS", "FAIL");
            session.setAttribute("VERIFY_MESSAGE", "인증에 실패했습니다.");
        }

        return "redirect:/coupon/result";
    }

    // 2) 결과 화면: 세션에서 값을 꺼내 보여줌
    @GetMapping("/result")
    public String couponResult(Model model, HttpSession session) {

        String status = (String) session.getAttribute("VERIFY_STATUS");
        VerifiedUser user = (VerifiedUser) session.getAttribute("VERIFIED_USER");
        String message = (String) session.getAttribute("VERIFY_MESSAGE");

        model.addAttribute("status", status);
        model.addAttribute("user", user);
        model.addAttribute("message", message);

        return "couponResult";
    }
}

DTO 예시

public class CouponCallbackForm {
    private String token;
    private String txId;

    public String getToken() { return token; }
    public void setToken(String token) { this.token = token; }

    public String getTxId() { return txId; }
    public void setTxId(String txId) { this.txId = txId; }
}
public record VerifiedUser(String userId, String userName, String grade) {}
public class CouponVerifyResponse {
    private boolean success;
    private String userId;
    private String userName;
    private String grade;

    public boolean isSuccess() { return success; }
    public String getUserId() { return userId; }
    public String getUserName() { return userName; }
    public String getGrade() { return grade; }
}

4. 세션이 “겹치지 않는” 이유 (초보자용)

세션은 보통 브라우저 쿠키(JSESSIONID)로 구분됩니다.

  • 크롬에서 로그인 → JSESSIONID=AAA → 세션 A
  • 엣지에서 로그인 → JSESSIONID=BBB → 세션 B

즉 브라우저가 다르면 기본적으로 세션이 다릅니다.
그래서 다른 사용자/다른 브라우저끼리는 겹치지 않아요.


5. 겹치는 것처럼 보일 수 있는 경우

1) 같은 브라우저에서 다른 계정으로 다시 인증

같은 브라우저는 같은 세션을 재사용할 수 있어요.
그때 같은 키(VERIFIED_USER)에 저장하면 값이 덮어쓰기 됩니다.

2) 같은 세션에서 동시에 여러 요청이 들어오는 경우

탭을 여러 개 열고 동시에 인증을 진행하면 마지막 저장값이 남는 경쟁 상황이 생길 수 있습니다.


6. 안전하게 쓰는 실무 팁 3가지

  1. 인증 성공 시 세션 재발급(세션 고정 방지, 계정 전환 깔끔)
  2. 세션에 값을 여러 개 흩뿌리지 말고 객체 하나로 묶어 저장
    • 예: session.setAttribute("VERIFIED_USER", user)
  3. 실패 메시지는 상세 내부 원인을 그대로 노출하지 말고 일반화된 메시지 사용

7. 한 줄 결론

외부 인증 연동의 핵심은:

“외부에서 받은 토큰을 서버가 다시 검증하고, 성공 결과를 세션에 저장해 다음 요청에서 꺼내 쓰는 것”

 

728x90
반응형

'개발 > spring boot' 카테고리의 다른 글

self-invocation 때문에 서비스 검증(@Validated)이 안 걸리는 문제 코드와, 실무에서 제일 흔한 해결책인 검증이 필요한 메서드를 다른 빈으로 분리 방법  (0) 2026.02.26
헥사고날 아키텍처 구조 설명  (0) 2026.02.12
resolveLocale(req)가 하는 일 - 이 요청의 언어/국가(locale)를 어떻게 정할까?  (0) 2025.10.15
Authentication을 SecurityContext 저장하는 사용방  (0) 2025.10.13
Spring Boot와 React 프로젝트를 하나로 합쳐서 개발하고 배포  (2) 2025.07.26
'개발/spring boot' 카테고리의 다른 글
  • self-invocation 때문에 서비스 검증(@Validated)이 안 걸리는 문제 코드와, 실무에서 제일 흔한 해결책인 검증이 필요한 메서드를 다른 빈으로 분리 방법
  • 헥사고날 아키텍처 구조 설명
  • resolveLocale(req)가 하는 일 - 이 요청의 언어/국가(locale)를 어떻게 정할까?
  • Authentication을 SecurityContext 저장하는 사용방
nix-be
nix-be
  • nix-be
    NiX
    nix-be
  • 전체
    오늘
    어제
    • 홈
      • 책
        • 오브젝트
      • 성장
        • jpa Querydsl 정리
        • 코딩테스트
        • 스프링 핵심 원리 - 기본편
      • 인프라
        • linux
        • vmware
        • CI&CD
        • 네트워크
        • docker
      • 개발
        • spring boot
        • spring security
        • JPA
        • java
        • thymeleaf
        • 이슈
        • jquery
        • javascript
        • 안드로이드
        • MyBatis
        • git
        • 과제
      • DB
        • postgreSql
        • mysql
        • redis
      • 잡다한것
        • 프로그램
        • 일상 관련
        • 프로젝트-주차관리
      • 회사
        • 티
        • 피
  • 블로그 메뉴

    • 홈
    • 개발
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
nix-be
(Spring) sso 외부 인증 토큰을 검증하고 세션에 사용자 정보를 저장하는 흐름
상단으로

티스토리툴바