기술 공부
스프링 시큐리티 (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);
반응형