기술 공부

스프링 시큐리티 (Spring Security)

랼랼 2022. 4. 11. 22:56

 

Email security vector created by macrovector - www.freepik.com

 

1. 개요

 

스프링 시큐리티는 서블릿 필터의 집합으로

디스페치 이전에 작동한다.

이 때 구현된 로직에 따라 원치 않는 HTTP 요청을 걸러내어 거절한다.

 

스프링 시큐리티가 FilterChainProxy라는 필터를 서블릿 필터에 넣어 작동한다.

 

2. 인증

OncePerRequestFilter 클래스를 상속하여 필터를 생성하고 한 요청당 한 번 실행되도록 한다.

 

3. 구현

 

1) 의존성 부여

build.gradle 의 dependencies 에 Spring security 추가

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.6.6'

 

2) Jwt 생성 및 확인

package com.example.demo.security;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

import org.springframework.stereotype.Service;

import com.example.demo.model.UserEntity;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;


@Slf4j
@Service
public class TokenProvider {
	private static final String SECRET_KEY ="안알랴쥼~";
	
	//토큰 생성
	public String create(UserEntity userEntity) {
		//기한은 지금부터 1일로 설정
		Date expireDate = Date.from(
				Instant.now().plus(1,ChronoUnit.DAYS)
				);
		//JMT Token 생성
		return Jwts.builder()
				//header에 들어갈 알고리즘, 시크릿키
				.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
				//payload의 내용
				.setSubject(userEntity.getId()) //sub
				.setIssuer("demo app") //iss
				.setIssuedAt(new Date()) //iat
				.setExpiration(expireDate) //exp
				.compact();
	}
	//토큰 확인
	public String validDateAndGetUserId(String token) {
		Claims claims = Jwts.parser()
				.setSigningKey(SECRET_KEY) //시크릿 키 세팅
				//Base64로 디코딩 및 파싱 후
				//시크릿 키로 헤더와 페이로드로 새 서명 생성 후 기존 서명과 비교
				//위조 시 예외 처리를 날림
				.parseClaimsJws(token)
				//비위조 시 몸통
				.getBody();
		//가져온 내용 중 subject, 즉 id를 리턴한다.
		return claims.getSubject();
	}
}

 

3) Jwt 권한 필터 구현

package com.example.demo.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
	
	@Autowired
	private TokenProvider tokenProvider;
	
	@Override
	protected void doFilterInternal(
    	HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			//요청에서 토큰 가져오기
			String token=parseBearerToken(request);
			log.info("Filter is running... token : {}",token);
			//토큰 검사
			if(token!=null&&!token.equalsIgnoreCase("null")) {
				//userId 가져오기, 위조된 경우 예외 처리됨
				String userId = tokenProvider.validDateAndGetUserId(token);
				log.info("Authenticated user ID : "+userId);
				//인증 완료 정보 넣기
				AbstractAuthenticationToken authentication =
					new UsernamePasswordAuthenticationToken(
                		userId, null, AuthorityUtils.NO_AUTHORITIES );
				//인증 완료 정보에 리퀘스트 세팅
				authentication.setDetails(
                	new WebAuthenticationDetailsSource().buildDetails(request));
				
				//SecurityContextHolder에 securityContext 등록
				
				//빈 securityContext 생성
				SecurityContext securityContext =
                	SecurityContextHolder.createEmptyContext();
				//securityContext에 인증정보 넣기
				securityContext.setAuthentication(authentication);
				//SecurityContextHolder에 securityContext 등록
				SecurityContextHolder.setContext(securityContext);
			}
		} catch (Exception e) {
			logger.error("Could not set user authentication in security context",e);
		}
		filterChain.doFilter(request, response);
	}
	//토큰 가져오기
	private String parseBearerToken(HttpServletRequest request) {
		//http 요청의 헤더 부분 중 Authorization 파싱하여 Bearer 토큰을 리턴한다.
		String bearerToken = request.getHeader("Authorization");
		//헤더의 모든 내용 확인
//		Enumeration eHeader =  request.getHeaderNames();
//		while(eHeader.hasMoreElements()) {
//			String name = (String)eHeader.nextElement();
//			String value = request.getHeader(name);
//			log.info("{} : {}",name,value);
//		}
		
//		log.info("BearerToken is "+bearerToken);
		if(StringUtils.hasText(bearerToken)&&bearerToken.startsWith("Bearer ")) {
			//앞 7글자 ("Bearer ") 이후 문자 리턴 (Token 내용만 리턴)
			return bearerToken.substring(7);
		}
		//토큰이 없으면 null 리턴
		return null;
	}
}

4) Spring Security 설정

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.csrf.CsrfFilter;

import com.example.demo.security.JwtAuthenticationFilter;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private JwtAuthenticationFilter jwtAuthenticationFilter;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//http 시큐리치 빌더
		http.cors() //기본 cors 설정, 이미 WebMvcConfig에서 설정했음
		.and()
		//csrf는 현재 사용 안함으로 disable(),
		//csrf() 는 Cross Site Requset Forgery 로 
		//위조 요청에 대해 보호를 한다.
		//그러나 jwt 방식은 서버에 인증정보를 저장하지 않기에 상관없음으로 disable 시킨다.
		.csrf().disable()
		//basic 인증 disable()
		.httpBasic().disable()
		//세션 기반이 아님을 선언 (jwt 방식)
		.sessionManagement()
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		.and()
		// "/" 와 "/auth/**" 경로에 대해 인증 검사 안함
		.authorizeRequests()
			.antMatchers("/","/auth/**").permitAll()
		// 나머지 경로에 대해 인증 검사
		.anyRequest()
			.authenticated();
		//매 요청마다 CorsFilter 실행한 후 jwtAuthenticationFilter 실행 => 안됨
		//순서를 변경, CsrfFilter after로 변경하니 작동
//		http.addFilterAfter(jwtAuthenticationFilter, BasicAuthenticationFilter.class);
		http.addFilterAfter(jwtAuthenticationFilter, CsrfFilter.class);
	}
	
}

5) 사용자 아이디 받아오기

 각 컨트롤러에서 사용자 아이디를 변수로 받아온다.

 다음은 한 예시이다.

//user로 등록된 글 검색
@GetMapping
public ResponseEntity<?> retrieveTodoList(@AuthenticationPrincipal String userId) {
    //1) 서비스의 메서드 retrieve() 메서드를 이용하여 Todo 리스트를 가져온다
    List<TodoEntity> entities = service.retrieve(userId);

    //2) 자바 스트림을 이용하여 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다
    List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());

    //3) 변환된 TodoDTO 리스트를 이용하여 ResponseDTO 초기화
    ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

    //4) ResponseDTO 리턴
    return ResponseEntity.ok().body(response);
}

@AuthenticationPrincipal 을 통해 userId를 가져올 수 있다.

[ 2) Jwt 생성 및 확인 ] 에서 다음과 같은 코드를 통해 authentication 객체에 userId를 principal로 저장하고,

SecurityContext에 등록하였다. @AuthenticationPrincipal는 이 SecurityContext의 authentication 객체를 이용하여 principal인 userId를 가져올 수 있다.

//인증 완료 정보 넣기
AbstractAuthenticationToken authentication
= new UsernamePasswordAuthenticationToken(userId, null, AuthorityUtils.NO_AUTHORITIES );
//인증 완료 정보에 리퀘스트 세팅
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

//SecurityContextHolder에 securityContext 등록

//빈 securityContext 생성
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
//securityContext에 인증정보 넣기
securityContext.setAuthentication(authentication);
//SecurityContextHolder에 securityContext 등록
SecurityContextHolder.setContext(securityContext);
반응형