본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.
목차
1. 스프링 시큐리티를 사용한 인증, 인가
2. JWT 생성 및 반환 구현
3. 스프링 시큐리티와 서블릿 필터
4. JWT를 이용한 인증 구현
1. 스프링 시큐리티를 사용한 인증, 인가
API 요청에 토큰 또는 아이디와 비밀번호를 보내는 작업을 스프링 시큐리티를 사용해 코드를 한 번만 짜고, 이 코드가 모든 API 요청에 수행되기 전에 실행되도록 설정및 구현을 해야한다.
2. JWT 생성 및 반환 구현
사용자 정보를 바탕으로 헤더와 페이로드를 작성하고 전자 서명한 후 토큰을 리턴해야 한다.
- JWT 관련 라이브러리를 디펜던시에 추가한다.
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
- security 패키지를 만들어 인증, 인가를 위한 모든 클래스를 해당 패키지 안에서 관리한다.
사용자 정보를 받아 JWT를 생성하는 클래스
package com.example.demo.security;
import com.example.demo.model.UserEntity;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
@Slf4j
@Service
public class TokenProvider {
private static final String SECRET_KEY = "NMA8JPctFuna59f5";
public String create(UserEntity userEntity) {
// 기한 지금으로부터 1일로 설정
Date expiryDate = Date.from(
Instant.now()
.plus(1, ChronoUnit.DAYS));
/*
{ // header
"alg":"HS512"
}.
{ // payload
"sub":"40288093784915d201784916a40c0001",
"iss": "demo app",
"iat":1595733657,
"exp":1596597657
}.
// SECRET_KEY를 이용해 서명한 부분
Nn4d1MOVLZg79sfFACTIpCPKqWmpZMZQsbNrXdJJNWkRv50_l7bPLQPwhMobT4vBOG6Q3JYjhDrKFlBSaUxZOg
*/
// JWT Token 생성
return Jwts.builder()
// header에 들어갈 내용 및 서명을 하기 위한 SECRET_KEY
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
// payload에 들어갈 내용
.setSubject(userEntity.getId()) // sub
.setIssuer("demo app") // iss
.setIssuedAt(new Date()) // iat
.setExpiration(expiryDate) // exp
.compact();
}
public String validateAndGetUserId(String token) {
// parseClaimsJws메서드가 Base 64로 디코딩 및 파싱.
// 즉, 헤더와 페이로드를 setSigningKey로 넘어온 시크릿을 이용 해 서명 후, token의 서명 과 비교.
// 위조되지 않았다면 페이로드(Claims) 리턴
// 그 중 우리는 userId가 필요하므로 getBody를 부른다.
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
- create( )
- JWT 라이브러리를 사용해 JWT 토큰을 생성한다.
- validateAndGetUserId( )
- 토큰을 디코딩 및 파싱하고 토큰 위조 여부를 확인한다.
- 이후 우리가 원하는 subject, 즉 사용자 아이디를 리턴한다.
토큰 생성 클래스를 사용해 토큰을 생성한 후 DTO에 반환하는 코드
@PostMapping("signin")
public ResponseEntity<?> authenticate(@RequestBody UserDTO userDTO){
UserEntity user = userService.getByCredentials(
userDTO.getEmail(),
userDTO.getPassword());
if(user != null){
final String token = tokenProvider.create(user);
final UserDTO responseUserDTO = UserDTO.builder()
.email(user.getEmail())
.id(user.getId())
.token(token)
.build();
return ResponseEntity.ok().body(responseUserDTO);
} else {
ResponseDTO responseDTO = ResponseDTO.builder()
.error("Login Failed")
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
3. 스프링 시큐리티와 서블릿 필터
- API가 실핼될 때마다 사용자를 인증해주는 부분을 구현할 것이다(스프링 시큐리티를 사용해서 구현).
- 토큰 인증을 위해 컨트롤러 메서드의 첫 부분마다 인증 코드를 작성해야 한다는 것이다.
- 이 고민은 서블릿 필터를 사용해 해결한다.
스프링 시큐리티란?
- 서블릿 필터의 집합을 스프링 시큐리티라 한다.
서블릿 필터란?
- 서블릿 실행 전에 실행되는 클래스들을 의미한다.
- 스프링이 구현하는 서블릿의 이름은 디스패처 서블릿이라고 한다.
- 즉, 서블릿 필터는 디스패처 서블릿이 실행되기 전에 항상 실행되는 클래스들을 의미한다.
- 우리는 서블릿 필터를 구현, 서블릿 필터를 서블릿 컨테이너가 실행하도록 설정해주면 된다.
- 서블릿 필터는 이름처럼 구현된 로직에 따라 원하지 않는 HTTP 요청을 걸러낼 수 있으며 조건에 부합되는 요청은 디스패처 서블릿으로 넘어와 컨트롤러에서 실행된다.
- 서블릿 필터는 여러개가 들어갈 수 있다. 그 이유는 걸러내고 싶은 조건이 많을 수록 필터의 클래스 크기가 커지기 때문에 여러 조건 마다 나눠서 필터로 연결해 사용할 수 있다.
스프링의 서블릿 필터
- 스프링 시큐리티 프로젝트를 추가하면 스프링 시큐리티가 FilterChainProxy라는 필터를 서블릿 필터에 끼워 넣어준다.
- 이 FilterChainProxy 클래스 안에서 내부적으로 필터를 실행시키는데 이 필터들이 스프링이 관리하는 스프링 빈 필터다.
서블릿 필터를 사용하기 위한 두 가지 작업
- 서블릿 필터 구현
- 서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려주는 설정 작업
4. JWT를 이용한 인증 구현
implementation 'org.springframework.boot:spring-boot-starter-security'
OncePerRequestFilter를 상속하는 필터 구현
package com.example.demo.security;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
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...");
//토큰 검사,JWT 인가 서버에 요청 없이 검증 가능
if(token != null && !token.equalsIgnoreCase("null")){
//userId 가져오기.
String userId = tokenProvider.validateAndGetUserId(token);
log.info("Authenticated user ID : "+userId);
//인증 완료
AbstractAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userId,
null,
AuthorityUtils.NO_AUTHORITIES
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authenticationToken);
SecurityContextHolder.setContext(securityContext);
}
} catch (Exception ex){
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String parseBearerToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Beare ")){
return bearerToken.substring(7);
}
return null;
}
}
- 요청 헤더에서 Bearer 토큰을 가져온다. parseBearerToken( )에서 이 작업이 이루어진다.
- 토큰 생성 클래스를 이용해 토큰을 인증한 다음 UsernamePasswordAuthenticationToken을 작성한다.
- 인증된 사용자를 SecurityContext에 저장한다. (인증 사용 여부를 사용해야할 때가 있기 때문이다.)
'Back-End > React.js, 스프링 부트, AWS로 배우는 웹 개발 101' 카테고리의 다른 글
[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][인증 백엔드 통합] - 스프링 시큐리티 (0) | 2022.07.01 |
---|---|
[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][인증 백엔드 통합] - User Layer 구현 (0) | 2022.06.22 |
[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][인증 백엔드 통합] - REST API 인증 기법 (0) | 2022.06.21 |
[React.js, 스프링 부트, AWS로 배우는 웹 개발 101] - 백엔드와 프론트엔드 연결 작업 그리고 CORS (0) | 2022.06.20 |
[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][프론트엔트 개발] - 서비스 개발2 (0) | 2022.06.16 |