마트철수
[071] JWT, Api Server Security 본문
2024.08.21(수)
Spring 12일차
Spring
PART01
- CH02. 스프링의 특징과 의존성 주입
- CH03.1 스프링 MVC의 기본 구조
- CH03.2 스프링 MVC의 Controller 1
- CH03.3 스프링 MVC의 Controller 2
- CH03.4 SpringLegacy 업데이트
- CH04.1 스프링과 MySQL Database
- CH04.2 MyBatis와 스프링 연동
- CH05.1 영속, 비즈니스 계층의 CRUD 구현
- CH05.2 비즈니스 계층
- CH05.3 프레젠테이션(웹) 계층의 CRUD 구현
- CH06.1 화면 처리
- CH06.2 File, Upload, Download
Part 4. Rest API
- CH07 Rest Controller
- CH08.1 OpenApi
- CH08.2 RestTemplate
- Spring Security
- CH10.2 로그인과 로그아웃 처리
- CH10.3 member
- CH10.4 UserDetails 사용하기
- CH11 Api Server Security 기본 설정
- CH11.1 JWT의 이해
- CH11.2 JWT 자바 라이브러리
CH11 Api Server Security 기본 설정
JSP에서 로그인한 사용자 정보 보여주기
<sec:authorize access="">
- 인증 여부 판단 및 접근 권한 체크
- isAnonymous(): 로그인을 하지 않은 경우 True
- isAuthenticated(): 로그인을 한 경우 True
# logout은 POST 요청 ... GET 요청 불가
# csrf.token 요청함 ... 없으면 404 error

SecurityConfig 전체 코드
SecurityConfig.java
- 문자셋 필터
- 경로별 접근 권한 설정
- 매번 달라져야함 (★)
- 예시로, 목록보기와 상세보기는 permitAll()
- 쓰기, 수정, 삭제는 authenticated()
// 문자셋 필터
public CharacterEncodingFilter encodingFilter() {
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceEncoding(true);
return encodingFilter;
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(encodingFilter(), CsrfFilter.class);
// ★ 경로별 접근 권한 설정
http.authorizeRequests()
.antMatchers("/security/all").permitAll()
.antMatchers("/security/admin").access("hasRole('ROLE_ADMIN')")
.antMatchers("/security/member").access("hasRole('ROLE_MEMBER')");
- 로그인/로그아웃 구현
- AuthenticationManager 구현 !!
- auth로 필요한 정보를 취합 및 등록
- 1) userDetailservice 2) passwordEncoder
http.formLogin() // GET요청 (개발자)
.loginPage("/security/login") // POST요청
.loginProcessingUrl("/security/login")
.defaultSuccessUrl("/"); // 어디로 redirect할지
http.logout() // 로그아웃 설정 시작
.logoutUrl("/security/logout") // POST: 로그아웃 호출 url
.invalidateHttpSession(true) // 세션 invalidate
.deleteCookies("remember-me", "JSESSION-ID") // 삭제할 쿠키 목록
.logoutSuccessUrl("/security/logout"); // GET: 로그아웃 이후 이동할 페이지
// 실전에서는 "/" 첫페이지로 이동하게 함
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
log.info("configure .........................................");
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
- 문자셋을 보안 필터 앞단에 설정하는 이유

보안 필터가 먼저 분석(resolve) → 그리고 문자셋 필터가 작용함 → 그래서 한글로 하면 다 깨짐
Api Server Security 기본 설정
Api 서버를 위한 기본 security 설정
- cors(Cross Origin Resource Sharing) 허용
- 서버주소: 포트번호(origin)
- 다른 서버와도 통신하는 것은 본래 origin 위반임
- 근데 허용해주면 됨 !!
Api Security 기본 설정
- 접근 제한 무시 경로 설정
- assets: 정적파일(이미지, css 등), api/member: 회원가입, 로그인
- /** : 하위경로까지 포함
// 접근 제한 무시 경로 설정 - resource
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/*", "/api/member/**");
}
- cross origin
// 다양한 도메인에서 서버에 요청을 보낼 수 있다
// 다른 서버도 접근 가능하도록 (다른 origin)
@Bean
public CorsFilter corsFilter() {
// CORS 설정을 적용한 url 소스 생성
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// CORS 설정을 위한 객체 생성
CorsConfiguration config = new CorsConfiguration();
// 자격 증명(쿠키, 인증 헤더 등)을 포함한 요청을 허용하도록 설정
config.setAllowCredentials(true);
// 모든 도메인에서 오는 요청 허용 ("은 모두라는 의미)
config.addAllowedOriginPattern("*");
// 모든 헤더 허용
config.addAllowedHeader("*");
// 모든 HTTP 메서드 허용(GET, POST, PUT, DELETE)
config.addAllowedMethod("*");
// 설정된 CORS 구성을 모든 경로("/**")에 적용 (하위 경로 포함)
source.registerCorsConfiguration("/**", config); // 하위 경로까지 포함
// 설정된 소스 기반으로 새로운 CorsFilter 반환
return new CorsFilter(source);
}
- 아래 구성은 많이 변경될 것임
- 필요한 기능을 넣기 위해서
@Override
public void configure(HttpSecurity http) throws Exception {
// 한글 인코딩 필터 설정
http.addFilterBefore(encodingFilter(), CsrfFilter.class);
http.httpBasic().disable() // 기본 HTTP 인증 비활성화
.csrf().disable() // CSRF 비활성화
.formLogin().disable() // formLogin 비활성화 관련 필터 해제
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션 생성 모드 설정
}
// Authentication Manger 구성
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
JWT의 이해
- Json Web Token
- JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web 토큰
- 가장 많이 사용되는 토큰의 형태
- JWT의 구조
- Header.Payload.Signature 세 가지로 구성
- 각 부분은 Base64로 인코딩되어 표현되며, 각각의 구성 요소는 .로 구분
- 암호화되어있지 않음 !!

디지털 서명의 원리
문서 T를 다시 받았는데, 수정 안 했는지에 대한 확신할 수 있나?
나에겐 원본이 없는 상태라서 비교 불가한 상태
내가 가지고 있는 키 프레임으로 hashCode를 만들어내서
T + key => hashCode 같은지 확인 !!
JWT의 Signature에서 secretKey를 합쳐서 해시코드를 만들어내냄
여기서 사용된 알고리즘이 Hs256임
이건 Header에서 확인 가능 !!
★ Payload(페이로드)
- 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨있다
- key: value → 클레임 !!
- 발급된 클레임
- iss: 토큰 발급자
- exp: 토큰 만료 시간
- 공개 클레임과 비공개 클레임
- 비공개 클레인은 사용자 정의 클레임
Spring Security + JWT 동작 원리
- 로그인
- 클라이언트에서 서버로 ID/PW로 로그인을 요청
# 사용자가 알게 - 서버에서 검증 과정을 거쳐 해당 유저가 존재하면, Access Token + Refresh Token 을 발급
ㄴ JSON 응답(성공)시, JWT를 발행해줌
ㄴ 근데 JWT의 유효기간은 짧음 그래서 Refresh Token을 같이 발급해줌
# 사용자 몰래
# Refrest Token은 복잡해서 해당 수업에서는 다루지 않음 - 클라이언트는 요청 헤더에 2번에서 발급받은 Access Token을 포함하여 API를 요청
ㄴ 유효하면 로그인한 사용자임을 확인
# 사용자 몰래
- 클라이언트에서 서버로 ID/PW로 로그인을 요청
- 토큰은 브라우저(클라이언트)가 관리

- Access Token
- 인증된 사용자가 특정 접근할 때 사용되는 토큰
- 확인이 가능한 토큰이기 때문에 비밀번호와 같은 걸 담으면 안됨
- 유효 기간이 지나면 만료(expired)
- Refresh Token
- Access Token의 갱신을 위해 사용되는 토큰
- 사용자가 지속적으로 인증 상태를 유지할 수 있도록 도와줌 (매번 로그인 다시 하지 않아도 됨)
JWT 자바 라이브러리
- 의존성 추가
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
- Secret Key 준비
- 암호화에 사용할 임의의 문자열
- 개발 시에는 직접 지정 (랜덤으로 만들어지는 게 더 좋음)
private String secretKey = "아주 긴 임의의 문자열 지정";
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); // BASE64 인코딩
// 실전 운영 시 자동으로 생성시키는 방법
private Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 서버가 재기동 되면 key문자열이 갱신되므로 기존 발급 토큰은 사용불가
// 유효 기간
static final long TOKEN_PERIOD= 1000L * 60L * 5L; // 테스트용 5분 – 만기 확인용
// 짧게 5분으로 설정해서, 자동으로 로그아웃되는지 확인하기 위함
- Payload 정보 구성
- Claims 객체
- date.getTime() + TOKEN_PERIOD : 현재 시간 + 만료 시간
- Claims 객체

- JWT 검증
- 유효시간 이전이면 true 리턴
- 토큰이 해석되지 않는 경우 또는 유효 시간 만료인 경우 예외 발생
ㄴ ExpiredJwtException: 유효 시간 만기 → 로그아웃 처리

- 하단 예외는 RuntimeException임 (필수는 아니지만, 확인해줘야함!)
- 401 ERROR 응답 필수 → 클라이언트: 로그아웃되었구나
예외 ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException
API 로그인
- LoginDTO
- Spring Security 규약에 따라 username, password 프로퍼티를 가짐
- UserInfoDTO
- 로그인 성공 시 응답에 포함시킬 사용자 정보
- AuthResultDTO
- 로그인 성공 결과를 나타내는 응답
JsonResponse
- 로그인 결괄르 필터에서 직접 Json 응답하기 윈한 유틸리티 클래스
- static <T> void send(HttpServletResponse response, T result) throws IOExcetion
- 200 응답
- Jackson으로 T를 직렬화한 후 response로 직접 전송
- static void sendError(HttpServletResponse response, HttpStatus status, String message)
- 응답 코드와 에러 메세지를 출력
API를 통한 로그인 절차
- JwtUsernamePasswordAuthenticationFilter
- 로그인 url과 로그인 성공/실패 처리기를 등록
- url은 /api/auth/login으로 결정 → 해당 url이 오면, login 요청으로 받아들임

- AuthenticationManager의 인증 절차는 formLogin 때와 동일
- 약속된 login 요청인 경우 로그인 절차 수행
- 생성자에서 의존성 주입
- AuthenticationManager
- LoginSuccessHandler
- LoginfailureHandler

이제 알고리즘 특강 제외
순수하게 Spring 공부가 가능한 일정이 8일 남았다.
마지막엔 Vue 연결도 하니깐,
Vue 교재를 다시 읽어보고 있는 중인데 ..
생각보다 더 어려운 것 같다.
'KB IT's Your Life > 교육' 카테고리의 다른 글
| [073] 알고리즘: Dynamic Programming(DP) (0) | 2024.08.23 |
|---|---|
| [072] API 로그인 및 사용자 인증 (0) | 2024.08.22 |
| [070] Spring 로그인과 로그아웃 처리 (0) | 2024.08.20 |
| [069] Rest API (0) | 2024.08.19 |
| [068] REST API (2) | 2024.08.14 |