기본 정보
- 주차/주제: 7주차 - 인증과 보안 기초
📚 학습 내용 요약
1. 인증(Authentication)과 인가(Authorization)의 차이점
A. 인증(Authentication) : 사용자의 신원을 입증하는과정
예시: 사용자가 로그인 화면에서 아이디와 비밀번호를 입력하여 신원을 증명하는 과정
스프링 시큐리티의 예: UsernamePasswordAuthenticationFilter: 사용자가 입력한 아이디와 비밀번호를 통해 인증 과정 처리
B. 인가(Authorization) : 사이트 특정부분에 접근할 수 있는지 파악 (사용자의 권한을 파악)
예시:
- 관리자 권한이 있는 사용자만 관리자 페이지에 접근 가능, 일반 사용자는 접근할 수 없도록 접근을 제어
- 파일공유시스템→ 내가 접근가능한 폴더만 사용가능
스프링 시큐리티의 예: FilterSecurityInterceptor: 사용자가 요청한 자원에 접근할 권한이 있는지 검사하여 접근 제어를 처리
- 인증은 집 현관문을 열 때 "이 집 주인이 맞나요?"라고 물어보는 것
- 인가는 집 안의 각 방을 들어갈 때 "내가 이 방에 들어갈 수 있나요?"라고 묻는 것과 같음
- 항상 인증 → 인가 순서로 진행(인증이 완료되지 않으면 인가 로직 자체가 동작하지 않음)
⇒ 스프링 시큐리티로 쉽게 구현 가능
스프링 시큐리티: 보안을 담당하는 하위 프레임워크
- CSRF 공격 방지 ( = 봉인스티커)
- CSRF 공격: 사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격
- Spring Security는 모든 POST, PUT, DELETE 요청에 대해 CSRF 토큰을 검증
- 세션고정 공격 방지 (= 비밀번호 변경 후 새 열쇠 발급)
- 사용자 인증 정보를 탈취하거나, 변조하는 공격
- 공격자가 미리 발급받은 세션 ID(쿠키)를 피해자에게 물고 들어가 로그인 유도
- Spring Security는 로그인 직후 항상 새로운 세션 ID를 발급하도록 기본 설정(SessionAuthenticationStrategy)
Spring Security
- 필터기반 javax.servlet.Filter로 인증·인가 처리
- 세션-쿠키 방식

- SecurityContextPersistenceFilter :
SecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체) 를 가져오거나 저장하는 역할 - LogoutFilter:
설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리함 - UsernamePasswordAuthenticationFilter: 인증관리자
폼 기반 로그인 시 사용되는 필터로 아이디 패스워드 데이터를 파싱하여 인증 요청을 위임인증이 성공하면 AuthenticationSuccessHandler, 실패하면 AuthenticationFailureHandler를 실행 - DefaultLoginPageGeneratingFilter:
사용자가 로그인 페이지를 지정하지 않았을 때 기본으로 설정하는 로그인 페이지 관련 필터 - BasicAuthenticationFilter:
요청 헤더에 있는 아이디와 패스워드를 파싱해 인증 요청
위임인증이 성공하면 AuthenticationSuccessHandler,
실패하면 AuthenticationFailureHandler를 실행 - RequestCacheAwareFilter:
로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청 처리 - SecurityContextHolderAwareRequestFilter:
HttpServletRequest 정보를 감싸고, 필터 체인 상의 다음 필터들에게 부가 정보 제공 - AnonymousAuthenticationFilter:
필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어준다 - SessionManagementFilter:
인증된 사용자와 관련된 세션 작업 진행, 세션 변조 방지 전략 설정, 유효하지 않은 세션에 대한 처리, 세션 생성 전략을 세우는 등의 작업을 처리한다 - ExceptionTranslationFilter:
요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달 - FilterSecurityInterceptor: 접근 결정 관리자
AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다.이 과정에서 이미 사용자 인증이 되어있으므로 유효한 사용자인지도 알 수 있음. 인가 관련 설정 가능

SecurityContextHolder(금고 열쇠 통)
- SecurityContext를 제공하는
static 메소드(getContext)를 지원한다.
SecurityContext (금고)
- 접근 주체와 인증에 대한 정보를 담고 있는 Context
즉, Authentication 을 담고 있다.
Authentication (보안카드 등록한 프로필)
- Principal과 GrantAuthority를 제공한다.인증이 이루어지면 해당 Athentication이저장
Principal (보안카드의 내정보)
- 유저에 해당하는 정보, 대부분의 경우 Principal로 UserDetails를 반환한다.
GrantAuthority
- ROLE_ADMIN, ROLE_USER 등 Principal이 가지고 있는 권한을 나타낸다.
스프링 시큐리티 인증 처리 과정

2. JWT(JSON Web Token)의 기본 구조와 장점 알아보기
기본구조
헤더(Header), 내용(Payload), 서명(Signature) 세 부분으로 구성되며, 각 부분은 마침표(.)로 구분

- 헤더(Header)
- 토큰타입 , 해싱 알고리즘 정보 담고 있음.
{ “typ”:”JWT”, 토큰 타입
“alg” : “HS256” } 해싱 알고리즘
- 내용(Payload)
- Payload는 클레임(claim)의 집합으로 → 내용을 한덩어리로
- 클레임: 키 값의 한쌍으로 이루어져 있음 → JWT에 데이터 넣을수있음
- 등록 클레임 (토큰에 대한 정보 ex. 토큰 발급자, 제목, 대상자, 만료시간, 발급 시간 등등)
- iss, sub, iat, exp 등
- 공개 클레임 : URI로 명명함 (공개되어도 상관 없음)
- 비공개 클레임
- 애플리케이션 정의 정보 - { "iss": "[ajufresh@gmail.com](mailto:ajufresh@gmail.com)", // 등록된 클레임 "iat": 1622370878, // 등록된 클레임 "exp": 1622372678, // 등록된 클레임 "https://shinsunyoung.com/jwt_claims/is_admin": true, // 공개 클레임 "email": "[ajufresh@gmail.com](mailto:ajufresh@gmail.com)", // 비공개 클레임 "hello": "안녕하세요!" // 비공개 클레임 }
- 사용자의 인증, 인가 정보가 담김
- 토큰과 관련된 정보
- 서명(Signature)
- 토큰 변조 여부를 확인하는 용도
- 헤더와 페이로드의 정보를 인코딩한 후, 비밀 키로 서명하여 생성
HMACSHA256(
base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
secretKey
)
장단점
JWT 이전의 세션 기반 인증
로그인
- 사용자 ID/PW 검증 후, 서버가 세션 객체(Map 형태)를 만들고 고유 ID(예: JSESSIONID)를 발급
- 이 ID를 쿠키로 돌려줌
요청 처리
- 클라이언트는 매 요청마다 쿠키의 세션 ID를 전송
- 서버는 이 ID로 DB/인메모리 캐시에 저장된 세션 정보를 조회 → 사용자 정보 로딩
단점
- 서버 상태 유지(Stateful): 접속자 수만큼 메모리/DB 부하 상승
- 확장성 문제: 여러 대 서버를 띄울 때 세션 동기화 혹은 Sticky Session 필요
- 쿠키 의존: 브라우저 설정에 따라 거부될 수 있고, 모바일 앱에서는 관리 불편
JWT 장점
- 웹 표준 준수 (RFC 7519)
- URL-safe: 쿼리 파라미터나 헤더에 안전하게 포함 가능
- Self-contained: 사용자 정보나 권한 정보를 토큰에 모두 담아 서버 상태 비저장(Stateless)
- 필요한 모든 정보를 하나의 객체에 담아서 전달하기 때문에 JWT 하나로 인증을 마칠 수 있다
- 확장성: 서버에 세션 저장소 불필요 → 인프라 비용 절감
- 설정 가능한 만료/발급 시간: exp, iat를 통해 만료 관리
JWT 단점
- 토큰 무효화 어려움
- stateless 방식 → 탈취된 토큰은 만료 전까지 유효 → 블랙리스트/키 회전 필요
- 크기 증가
- Header.Payload.Signature 3부분, 각 파트에 들어가는 정보(클레임)가 많아질수록 길이가 커짐
- HTTP 요청마다 Authorization: Bearer {JWT} 헤더에 수백~수천 바이트의 문자열을 전송해야 함
- 보안 이슈
- XSS·CSRF 공격에 취약할 수 있어 HTTPS, Secure/Cookie 설정 권장
- XSS(크로스 사이트 스크립팅) : 악성 스크립트 삽입 → 방문 이용자가 스크립트 실행되도록 함
- XSS·CSRF 공격에 취약할 수 있어 HTTPS, Secure/Cookie 설정 권장
보완책
- 리프레시 토큰: 액세스 토큰 만료 시 재발급용으로 사용
- 액세스 토큰이 만료되어 새로운 액세스 토큰을 만들때 사용

- 블랙리스트 or 키 회전(Key Rotation) 전략
- 짧은 액세스 토큰 수명 + 안전한 저장소 (예: HttpOnly Cookie)
JWT 적용 방식 HTTP request
- Stateless 인증
- 세션 대신 HTTP 헤더 Authorization: Bearer {token} 사용
- 서버가 클레임(Claim) 기반으로 토큰 서명 확인 → 사용자 정보(Subject, 권한) 복원
- 커스텀 필터 (OncePerRequestFilter 상속)
- 요청마다 JWT 추출 → 유효성 검사 → Authentication 생성 → SecurityContextHolder저장
- 헤더에서 JWT 추출
- 서명·만료일 검사
- 토큰에서 사용자 정보 파싱 → UsernamePasswordAuthenticationToken 생성
“휴대폰 QR 체크인”
QR 체크인 과정 JWT 인증 흐름
1. QR 코드 제시
“한 번만 앱을 켜고 QR 보여주기”
클라이언트가 Authorization: Bearer {JWT} 헤더에 토큰 담아 요청
- 직원이 스캔하여 확인
“QR 스캔 → 유효 기간·위조 검사→ OK OncePerRequestFilter가 토큰 추출 → 서명·만료일 검증 → Authentication 생성 - 그 뒤로는 QR만 보여주면 통과“매번 신분증 없이 QR로만 입장 OK” 매 요청마다 토큰 검증 → 상태 비저장(Stateless) 인증 유지
- QR 잃어버리거나 훼손 시 입장 불가
“유효 기간 지난 QR 혹은 변조된 QR은 차단” 만료된 토큰(exp) 또는 서명 불일치 시 인증 실패(403)
💡 새롭게 알게 된 점 & 어려웠던 부분
- 보안과 관련해 처음 배우는 내용이라, 이해하는데 시간이 꽤 걸림.
- 배울 범위가 방대해 천천히 개념을 잡아가야 할듯..
🚀 과제/실습 결과
인증, 인가 실습 -> 스프링 시큐리티
UserDetails 클래스
@RequiredArgsConstructor
@Service
// 스프링 시큐리티에서 사용자 정보를 가져오는 인터페이스
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
// 사용자 이름(email)을 통해 사용자 정보를 가져오는 메서드
@Override
public User loadUserByUsername(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException((email)));
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserDetailService userService;
// 스프링 시큐리티 비활성화 (모든 기능 사용하지 못하게)
// 일반적으로 정적 리소스에 인증, 인가 사용
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers(new AntPathRequestMatcher("/static/**"));
}
// 특정 HTTP 요청에 대한 보안 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth // 인증, 인가 설정
.requestMatchers( // 특정 요청과 일치하는 url 액세스
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/signup"),
new AntPathRequestMatcher("/user")
).permitAll() // 모든 사용자에게 허용(로그인, 회원가입은 인증 필요 없음)
.anyRequest().authenticated())
//anyRequest()는 모든 요청을 의미하며,
// authenticated()는 인증된 사용자만 접근할 수 있도록 설정
.formLogin(formLogin -> formLogin // 폼 기반 로그인
.loginPage("/login") // 로그인 페이지 경로
.defaultSuccessUrl("/articles") // 로그인 성공 시 이동할 경로
)
.logout(logout -> logout // 로그아웃 설정
.logoutSuccessUrl("/login") // 로그아웃 성공 시 이동할 경로
.invalidateHttpSession(true) // 세션 무효화
)
.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화 (실습때문에 비활성화)
.build();
}
// 인증 매니저 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService); // 사용자 정보 서비스 설정
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}
// 패스워드 인코더로 사용할 빈 설정
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest dto) {
return userRepository.save(User.builder()
.email(dto.getEmail())
// 비밀번호 암호화
.password(bCryptPasswordEncoder.encode(dto.getPassword()))
.build()).getId();
}
}
# Spring JPA
spring.application.name=springboot-developer
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true
# datasource
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
# Hibernate DDL
spring.jpa.hibernate.ddl-auto=update
# H2
spring.h2.console.enabled=true

JWT 실습
- 토큰 생성
- 올바른 토큰인지 유효성 검사
- 토큰에서 필요한 정보 가져오기
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
// 사용자 정보와 만료 기간을 입력받아 JWT를 생성합니다.
// 만료 시간을 계산한 뒤, makeToken 메서드를 호출합니다.
public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
// JWT 토큰을 생성하는 메서드
// JWT의 헤더, 페이로드, 서명을 설정합니다.
// 설정된 정보를 기반으로 JWT 문자열을 생성
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 JWT 타입 설정
// 내용 iss: ajufresh@gmail.com (properties에서 가져옴)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now) // 내용 iat : 발급 시간
.setExpiration(expiry) // 내용 exp : expiry 멤버 변숫값
.setSubject(user.getEmail()) // 내용 sub : 사용자 이메일
.claim("id", user.getId()) // 클레임 id : 사용자 id
//서명: 비밀값과 함께 해시값을 H256 알고리즘으로 암호화
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
// JWT 토큰을 유효성 검증하는 메서드
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) //비밀값으로 복호화
// Properties에서 가져온 비밀값으로 복호화
.parseClaimsJws(token);
return true;
} catch (Exception e) {// 복호화 과정에서 에러가 나면 유효하지않은 토큰
return false;
}
}
// JWT 토큰에서 사용자 인증 정보를 가져오는 메서드
// 토큰을 받아 인증정보를 담은 Authentication 객체를 생성
public Authentication getAuthentication(String token) {
// 1. 토큰 복호화 및 클레임 추출
Claims claims = getClaims(token); // JWT 토큰을 복호화하고, 클레임(Claims) 정보를 가져옵니다.
//클레임은 JWT의 페이로드에 포함된 데이터로, 여기서는 사용자 이메일(sub)과 기타 정보를 포함
// 2. 권한 설정
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
// Set<SimpleGrantedAuthority>를 사용하여 사용자 권한을 설정합니다. (ROLE_USER라는 권한을 부여)
// 3. Authentication 객체 생성
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject
(), "", authorities), token, authorities);
//UsernamePasswordAuthenticationToken 객체를 생성하여 반환
//이 객체는 스프링 시큐리티에서 인증 정보를 담는 데 사용됩니다.
//첫 번째 인자로 들어가는 User는 스프링 시큐리티에서 제공하는 org.springframework.security.core.userdetails.User 클래스를 사용해야 합니다
}
// JWT 토큰에서 사용자 ID를 가져오는 메서드
public Long getUserId(String token) {
Claims claims = getClaims(token); // JWT 토큰을 복호화하고, 클레임(Claims) 정보를 가져옵니다.
// 클레임 정보 반환 -> 클레임에서 id 값 추출
return claims.get("id", Long.class);
}
private Claims getClaims(String token) {
return Jwts.parser() // 클레임 조회
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer;
private String secretKey;
}
👥 스터디 피드백
Q. Refresh Token도 탈취될 경우, Access Token의 보완책으로 나온 의미가 없는데, 이를 어떻게 처리하면 되는가?
A. Refresh Token의 유효기간을 짧게 설정하고, 2중 3중으로 암호화를 하는 방법이 있다.
Q. Access/Refresh Token 전략 설계 -> Access Token만 사용할때?
A. 짧은 유효기간 (15분~1시간)으로 Access Token을 설정하기.
Q. 토큰 저장 위치는 어디로 할까? (쿠키 vs 로컬스토리지)
A. 쿠키 -> XSS 공격 방지, 자동으로 쿠키가 전송되어 편리
로컬스토리지 -> XSS 취약, 자동 전송되지 않으므로 CSRF 공격에 안전, 토큰을 수동으로 추가
보안측면에선 쿠키를 사용하는것이 더 안전.