
1. Spring Securiy 6.0
관리자가 여러 행동을 할수 있는 프로젝트인 만큼 관리자, 유저, 비로그인 유저 허용 페이지를 따로 나눴다.
Jwt필터를 통해서 인증, 인가를 처리하도록 했다.
package com.kmbbj.backend.global.config.security;
import com.kmbbj.backend.global.config.jwt.filter.TokenAuthenticationFilter;
import com.kmbbj.backend.global.config.jwt.service.TokenService;
import com.kmbbj.backend.global.config.jwt.util.JwtTokenizer;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 설정 클래스
* 애플리케이션의 보안 구성을 정의
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// JWT util
private final JwtTokenizer jwtTokenizer;
private final TokenService tokenService;
// 모든 유저 허용 페이지
String[] allAllowPage = new String[]{
"/", // 메인페이지
"/error", // 에러페이지
"/test/**", // 테스트 페이지
"/auth/refreshToken" // 토큰 재발급 페이지
};
// 관리자 유저 허용 페이지
String[] adminAllowPage = new String[]{
"/", // 메인페이지
"/error", // 에러페이지
"/test/**", // 테스트 페이지
"/auth/refreshToken" // 토큰 재발급 페이지
};
// 비로그인 유저 허용 페이지
String[] notLoggedAllowPage = new String[]{
"/auth/login", // 로그인 페이지
"/auth/join" // 회원가입 페이지
};
/**
* 보안 필터 체인
*
* @param http 수정할 HttpSecurity 객체
* @return 구성된 SecurityFilterChain
* @throws Exception HttpSecurity 구성 시 발생한 예외
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 유저별 페이지 접근 허용
http.authorizeHttpRequests(auth -> auth
.requestMatchers(allAllowPage).permitAll() // 모든 유저
.requestMatchers(adminAllowPage).hasRole("ADMIN") //관리자
.requestMatchers(notLoggedAllowPage).not().authenticated() // 비로그인 유저
.anyRequest().authenticated()
);
// 세션 관리 Stateless 설정(서버가 클라이언트 상태 저장x)
http.sessionManagement(auth -> auth.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// cors 허용
http.csrf(csrf -> csrf.disable());
// 로그인 폼 비활성화
http.formLogin(auth -> auth.disable());
// http 기본 인증(헤더) 비활성화
http.httpBasic(auth -> auth.disable());
//jwt 필터를 한 번 타서 검사하도록 그리고 인증하도록 설정
http.addFilterBefore(new TokenAuthenticationFilter(jwtTokenizer, tokenService), UsernamePasswordAuthenticationFilter.class);
// SecurityFilterChain을 빌드 후 반환
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. Jwt
jwt의 access와 refresh의 사용 방법은 다른 글에서 다루었다.
이전 내가 진행했던 프로젝트에서는 jwt를 header의 Authoriztion에 모두 담아서 교환하고 프론트엔드에서는 필요한 요청에 access나 refresh를 사용하는 방식으로 진행했었는데 이번에 cookie를 chrome에서는 자동으로 요청에 담아준다는 걸 알게되어서 로직을 약간 변경했다.


기존 알고 있던 방식의 토큰 사용 방식은 다음과 같다 여기서 수명이 긴 refresh를 탈취당한다면 공격자에게 매우 취약해 지지 않을까라는 생각을 했고 다음과 같은 방식으로 변경했다.
- access는 cookie에 담겨어 어느 요청이던지 사용할 수 있게 한다. 이로인해서 어떤 요청을 보내든 프론트에서는 딱히 access를 신경쓸 필요를 없게 한다.
- refresh는 Authoriztion에 담기게 되어 프론트에서는 이를 받았을때 로컬 혹은 세션 스토리지에 저장하게 되고 refresh 토큰이 사용이 필요할때만 요청에 담아서 사용하게 된다.
- refresh 탈취를 보호하기 위해서 access를 사용할때마다 다시 refresh와 access를 갱신해서 사용자에게 보내준다. 이로인해서 공격자가 refresh를 탈취해서 공격할 수 있는 시간이 줄어든다.
- redis에 사용자 refresh를 보관해서 지금 사용하고 있는 refresh가 맞는지 확인하고 틀리다면 access토큰을 발급해주지 않는다.
access토큰은 시간을 짧게 가저감으로써 redis에 저장할 필요가 없다고 생각했다.

이러한 방식을 쓸때 단점은 없는가?
- Restfull 방식을 사용못함 :
redis에 refresh를 보관한다라는 뜻은 완전한 무상태성을 지키지 못한 즉 세션을 사용하는 것과 다름이 없다. - 상당한 비용 :
refresh를 재발급 해주고 redis를 사용해서 저장해야하는 만큼 메모리와 로직을 진행하는 서버의 비용이 생긴다. 작아보일수 있으나 0과 1은 다르다.
그래도 선택한 이유는 내가만드는 프로젝트는 보안이 무엇보다 우선시 되어야한다. 일단 가상의 돈이라지만 돈이라는 것을 거래하는 사이트이고 돈에서 가장 중요한 점은 보안이기에 보안성을 가져간다.
추가하고 싶은 것
- Secure 속성 설정 - HTTPS를 통해서만 전송될 수 있도록 함
- SamSite 속성 설정 - CSRF 공격을 예방하도록 하는 설정
이를 사용하고 싶으나 이는 프론트엔드에서도 신경을 써야하는 부분이고 팀원 그리고 나도 react에는 지식이 부족하다보니 아쉬움으로 남겨뒀다. 다음은 access토큰을 사용할때 새로 응답을 만들며 access랑 refresh를 넣어주는 로직이다.
/**
* access토큰을 사용할때마다 새로운 refresh랑 access를 만들어주는 코드
* redisdp 새로운 refesh를 넣어준다.
*
* @param response 응답
* @param token 사용할 과거 refresh토큰
*/
private void makeNewResponseTokens(HttpServletResponse response, String token) {
Claims claims = jwtTokenizer.parseAccessToken(token);
Long userId = claims.get("userId", Long.class);
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
Authority authority = Authority.valueOf(claims.get("authority", String.class));
String newAccessToken = jwtTokenizer.createAccessToken(userId, email, nickname, authority);
String newRefreshToken = jwtTokenizer.createRefreshToken(userId, email, nickname, authority);
// 새로운 리프레시 토큰을 데이터베이스에 저장
tokenService.saveOrRefresh(new redisToken(userId, newRefreshToken, tokenService.calculateTimeout()));
// 새로운 액세스 토큰을 쿠키에 추가
Cookie accessTokenCookie = new Cookie("Access-Token", newAccessToken);
accessTokenCookie.setPath("/"); // 모든 경로에서 유효
accessTokenCookie.setMaxAge((int) jwtTokenizer.getAccessTokenExpire()); // 액세스 토큰 만료 시간 설정
response.addCookie(accessTokenCookie);
// 새로운 리프레시 토큰을 응답 헤더에 추가
response.setHeader("Refresh-Token", newRefreshToken);
}'Project : 그때 살껄;;.. > 개발일지' 카테고리의 다른 글
| [멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 시스템 아키텍처 (0) | 2024.09.09 |
|---|---|
| [멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 디스코드 Github 알림 (0) | 2024.08.26 |
| [멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 거래 시스템 설계 (2) | 2024.08.17 |
| [멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] Auth + Test + CORS + Header + Swagger Error (0) | 2024.08.09 |
| [멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 프로젝트 초기 설계 (0) | 2024.08.05 |