Spring Security를 사용하지 않으면, 사용자의 요청은 다음과 같은 순서로 처리된다
요청 → DispatcherServlet → Controller → Service → Repository
여기에는 보안 관련 기능이 없음 . 즉, 누가 요청했는지, 권한이 있는지 등은 고려하지 않고 그냥 요청을 처리한다. 그래서 인증(Authentication)과 인가(Authorization) 같은 기능을 직접 구현해야 함.
spring Security는 스프링에서 인증/인가를 처리하는 강력한 보안 프레임워크로 다음과 같은 기능을 제공한다.
로그인 처리 (폼 로그인, OAuth2, JWT 등)
비밀번호 암호화
인증된 사용자 정보 저장 및 관리
인가 처리 (어떤 권한이 있어야 어떤 URL에 접근 가능한지 등)
implementation 'org.springframework.boot:spring-boot-starter-security'
spring security 라이브러리를 추가하면 요청 흐름이 아래처름 바뀐다.
요청 → Security Filter Chain → DispatcherServlet → Controller → ...
1. 로그인 요청: 사용자가 ID/PW 전송 → 서버는 확인 후 JWT 토큰 발급
2. 클라이언트 저장: 클라이언트(브라우저, 앱 등)는 토큰을 로컬에 저장
3. 요청 시 헤더에 토큰 첨부: ```Authorization: Bearer <JWT 토큰>```
4.필터에서 토큰 검사:
- 토큰 유효성 검사
- 토큰 안의 사용자 정보로 Authentication 객체 생성
- SecurityContext에 저장
5. 인가 처리:사용자 권한이 요청 리소스에 접근 가능한지 판단 후 허용/거부
개념 | 설명 |
---|---|
Authentication | “누군지 확인” (로그인, 토큰 검사 등) |
Authorization | “권한이 있는지 확인” (접근 허용/차단) |
Filter | 요청을 가로채서 보안 처리하는 역할 |
SecurityContext | 현재 로그인한 사용자 정보를 저장하는 공간 |
JWT | 토큰 기반 인증 시 사용하는 방식 (Spring Security에서 직접 지원하지는 않고 우리가 구현) |
@Configuration
@RequiredArgsConstructor
public class Securityconfigs {
private final JwtAuthFilter jwtAuthFilter;
@Bean//return 하는 객체를 싱글톤으로 만듦
public SecurityFilterChain myFilter(HttpSecurity httpSecurity) throws Exception {
//내가 만든 securityFilterChain을 만들어서 리턴
return httpSecurity
.cors(cors->cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)// 보안 공격 csrf 대비 안함 보안 공격을 대비할 수 있는 방안이 많아서 filter에서 비활성
.httpBasic(AbstractHttpConfigurer::disable)// HttpBasic 비활성화 우리는 토큰 기반의 인증을 할거여서 비활성
//특정 url 패턴에 대해서는 Authentication 객체를 요구하지 않음(인증처리 제외)
.authorizeHttpRequests(a-> a.requestMatchers("/member/create","/member/doLogin").permitAll().anyRequest().authenticated())
.sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//세션 방식을 사용하지 않겠다는 의미
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) //위 특정 url을 제외하고 사용할 인증 필터를 추가
.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("*")); // 모든 http메서드 허용
configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더값 허용
configuration.setAllowCredentials(true);// 자격 증명 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",configuration);
return source;
}
}
@Bean//return 하는 객체를 싱글톤으로 만듦
public SecurityFilterChain myFilter(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.cors(cors->cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(a-> a.requestMatchers("/member/create","/member/doLogin").permitAll().anyRequest().authenticated())
.sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(...)
a -> a.requestMatchers("/member/create", "/member/doLogin").permitAll()
.anyRequest().authenticated()
/member/create
, /member/doLogin
→ 누구나 접근 허용 .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); // 이 도메인만 허용
configuration.setAllowedMethods(Arrays.asList("*")); // GET, POST 등 모든 메서드 허용
configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보를 포함한 요청 허용 (ex: 쿠키, Authorization 헤더)
jwt 토큰은 header, payload, signature로 구성 되어있다. header는 토큰에 어떤 알고리즘으로 생성 되었는지 타입이 뭔지, payload에는 실질적인 정보가 들어간다. signature에는 payload의 토큰이 우리 서버에서 생성 되었는지 확인하는 정보가 들어간다.(signature는 암호화되어 있다.)
{
"alg": "HS256", // 어떤 알고리즘으로 서명했는지
"typ": "JWT" // 토큰 타입 (고정적으로 JWT)
}
{
"sub": "user123",
"role": "ROLE_USER",
"exp": 1686200000 // 만료 시간 (Unix timestamp)
}
Signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
구분 | 인코딩 | 암호화 |
---|---|---|
목적 | 데이터 표현 변환 | 보안 보호 |
복호화 | 누구나 가능 (Base64 등) | 비밀키가 있어야 가능 |
예시 | Base64, URL 인코딩 | AES, RSA, HMAC 등 |
JWT 적용 | Header, Payload → 인코딩 | Signature → 해시 기반 서명 |
💡 결론:
Header와 Payload는 Base64 인코딩 (누구나 볼 수 있음)
Signature는 서명(Hash) 되어 있어서 위조 방지용
→ JWT는 무결성(내용이 안 바뀌었는지) 확인은 가능하지만 **기밀성(내용을 숨김)**은 제공하지 않음
@PostMapping("doLogin")
public ResponseEntity<?> doLogin(@RequestBody MemberLoginReqDto memberLoginReqDto){
// email, password 검증
Member member = memberService.login(memberLoginReqDto);
//일치할 경우 access 토큰 발행
String jwtToken = jwtTokenProvider.createToken(member.getEmail(), member.getRole().toString());
Map<String, Object> loginInfo = new HashMap<>();
loginInfo.put("id",member.getId());
loginInfo.put("token",jwtToken);
return new ResponseEntity<>(loginInfo,HttpStatus.OK);
}
@Component
public class JwtTokenProvider {
private final String secretKey;
private final int expiration;
private Key SECRET_KEY;
public JwtTokenProvider(
//secretKey와 expiration을 yaml에서 가져옴
@Value("${jwt.secretKey}") String secretKey,
@Value("${jwt.expiration}") int expiration
) {
this.secretKey = secretKey;
this.expiration = expiration;
//1. java.util.Base64.getDecoder().decode(secretKey) -> secretKey를 decoding
//2. new SecretKeySpec(..., SignatureAlgorithm.HS512.getJcaName()); -> 1번의 결과를 '암호화' (복구 불가)
this.SECRET_KEY = new SecretKeySpec(java.util.Base64.getDecoder().decode(secretKey), SignatureAlgorithm.HS512.getJcaName());
}
public String createToken(String email, String role){
//claims: jwt의 payload에 해당하는 부분
//.setSubject(): 대표값 세팅 여기서는 email을 대푯값으로 사용
Claims claims = Jwts.claims().setSubject(email);
claims.put("role",role);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now) //발행시간 세팅
.setExpiration(new Date(now.getTime() + expiration*60*1000L)) // 만료일자 세팅
.signWith(SECRET_KEY) // 서명: 암호화 시킨 secret_key 값으로 서명
.compact();
return token;
}
}
<@Component
public class JwtAuthFilter extends GenericFilter {
@Value("${jwt.secretKey}")
private String secretKey;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//request에서 jwt를 꺼내서 확인한후 인증 되면 dofilter 인증이 안되면 에러
// dofilter로 돌아갈때 우리가 만든 토큰이 맞으면 authentication 객체를 만들어야함
//
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
//토큰 꺼내기
String token = httpServletRequest.getHeader("Authorization");
try{
//토큰이 없는 경우에는 그냥 지나침(authentication)
if(token!=null){
//토큰을 넣어서 왔을 때
if(!token.substring(0,7).equals("Bearer ")){
throw new AuthenticationServiceException("Bearer 형식이 아닙니다.");
}
String jwtToken = token.substring(7);
// 토큰 검증 및 claims 추출
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey) //
.build()
.parseClaimsJws(jwtToken)
.getBody();
//Authentication 객체 생성\
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_"+claims.get("role")));
UserDetails userDetails = new User(claims.getSubject(),"", authorities);
//authentication 객체에는 이메일과 권한이 들어가 있음
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
//SecurityContextHolder 안에 있는 context 안에 authentication 객체가 있음
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest,servletResponse);
}catch (Exception e){
e.printStackTrace();
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write("invalid token");
}
//토큰이 있을 때만 검증
}
}
String token = httpServletRequest.getHeader("Authorization");
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwtToken)
.getBody();
UserDetails userDetails = new User(claims.getSubject(), "", authorities);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);