728x90
반응형
외부 시스템(SSO, 인증 서버, 쿠폰 인증 서버 등)과 연동할 때 흔히 나오는 패턴이 있습니다.
- 외부 서버가 인증 결과(토큰) 를 우리 서버에 보내줌
- 우리 서버는 그 토큰을 외부 서버에 다시 물어봐서 검증함
- 검증 성공 시, 사용자 정보를 세션(HttpSession) 에 저장
- 이후 화면/다음 요청에서 세션에서 꺼내 사용
이 글에서는 “쿠폰 인증 서버” 예시로 설명하겠습니다. (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가지
- 인증 성공 시 세션 재발급(세션 고정 방지, 계정 전환 깔끔)
- 세션에 값을 여러 개 흩뿌리지 말고 객체 하나로 묶어 저장
- 예: session.setAttribute("VERIFIED_USER", user)
- 실패 메시지는 상세 내부 원인을 그대로 노출하지 말고 일반화된 메시지 사용
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 |