JWT & 스프링 시큐리티 & 카카오 소셜로그인 API 활용방법 (1)
IT 연합동아리 코테이토 6주 차 세션에서 인증과 인가에 관한 주제를 다뤘었다. 나는 해당 동아리의 교육팀이기 때문에 세션 진행 전 발표자의 자료를 검토하고 동아리 부원들이 풀 문제를 출제하는 역할을 한다. 요번 주제가 프로젝트 첫 단계인 로그인 구현 부분과 밀접한 부분이 많아 관련 내용을 정리하고, 더 나아가 카카오 소셜로그인 api를 받아와 간단하게 로그인을 구현하는 방법에 대해서 정리하고자 한다.
인증과 인가
인증 : 사용자의 신원을 검증하는 행위입니다. ex) 도서관 출입
인가 : 사용자에게 특정 리소스나 기능에 액세스 할 수 있는 권한을 부여하는 프로세스입니다. ex) 데스크 출입 권한
인가는 인증을 받은 사용자가 할 수 있는 행동을 허락하는 행위입니다.
인가를 받기 위해서는 인증 과정이 필수적으로 필요하다고 볼 수 있죠!
토큰
토큰 : 사용자를 식별할 수 있는 문자열로, 아이디와 비밀번호 대신 사용할 수 있습니다.
ex) Bearer Token
Authorization: Bearer NDjfdoijvklcmvxldklsbaufdsbkjabdiocbldsbclkjsd
토큰 생성과 전달 순서
1) 클라이언트가 로그인을 하면 서버는 토큰을 생성하여 클라이언트에게 넘겨줍니다.
2) 클라이언트는 요청할 때마다 서버가 넘겨준 토큰을 요청 헤더에 담아 요청합니다.
3) 서버는 요청을 받을 때마다 요청 헤더의 토큰을 서버가 생성했던 토큰과 비교하여 클라이언트를 인증합니다.
JWT
JWT : JSON Web Token으로 JSON 형식에 대한 토큰의 표준 규격
형식 : {header}. {payload}. {signature}
header 내용 : 토큰이 어떤 알고리즘으로 인코딩 되어있는지 알려줍니다.
payload 내용 : 토큰에 저장되는 정보가 인코딩 되어 있습니다.
singarure 내용 : payload를 비밀키를 통해 인코딩하여 나온 특수 문자열입니다. 토큰의 유효성을 검사하는 데 사용됩니다.
JWT 토큰 생성 메커니즘
클라이언트가 최초 로그인 시, 서버는 클라이언트의 아이디와 비밀번호를 서버에 저장된 아이디와 비밀번호와 비교합니다. 인증된 사용자인 경우, 사용자 정보를 이용해 {header}. {payload}를 만들고 서버에서 만든 시크릿 키를 사용하여 전자서명한 뒤 인코딩하여 클라이언트에게 반환합니다. 따라서 JWT 인증에서는 인증 서버가 필요 없고, 토큰에 포함된 서명으로 인증을 수행해야 합니다.
스프링 시큐리티
서블릿 필터 : 서블릿 실행 전에 실행되는 클래스, 백엔드 서버 동작 전에 작동하는 필터입니다.
스프링 시큐리티의 서블릿 필터를 사용하면 인증 부분의 중복을 없앨 수 있습니다.
백엔드 개발자는 서블릿 필터를 구현하고, 서블릿 필터를 서블릿 컨테이너가 실행하도록 설정해 주면 됩니다.
1. JWT 관련 라이브러리를 디펜던시에 추가하기
- build.gradle
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
2. 사용자 정보를 받아 JWT를 생성하는 일을 하는 TokenProvider
- TokenProvider
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 = "dkfspdoivlkrsn;lksmoig43yg9hdkgn";
public String create(UserEntity userEntity){
// 기한 지금으로 부터 1일로 설정
Date expiryDate = Date.from(Instant.now().plus(1, ChronoUnit.DAYS));
// 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 claims = Jwts.parser() // 페이로드에 담긴 key와 value를 claim이라고 합니다.
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
3. 서블릿 필터 설정
@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 ... ");
if(token != null && !token.equalsIgnoreCase("null")){
String userId = tokenProvider.validateAndGetUserId(token);
log.info("Authenticated user ID : " + userId);
AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userId,
null,
AuthorityUtils.NO_AUTHORITIES
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
}
}catch (Exception ex){
logger.error("Could not set user authentication in security context", ex);
} // 다음 ServletFilter 실행
filterChain.doFilter(request, response);
}
private String parseBearerToken(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")){
return bearerToken.substring(7);
}
return null;
}
}
4. 서블릿 필터 설정
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
5. WebSecurityConfig 클래스 생성
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfiguration {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception{
// https 시큐리티 빌더
http.cors()
.and()
.csrf()
.disable()
.httpBasic()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/", "/auth/**").permitAll()
.anyRequest()
.authenticated();
http.addFilterAfter(
jwtAuthenticationFilter,
CorsFilter.class
);
}
}
'Back-End > Project' 카테고리의 다른 글
JWT & 스프링 시큐리티 & 카카오 소셜로그인 API 활용방법 (2) (0) | 2024.05.05 |
---|---|
인텔리제이 오류 모음 (0) | 2024.02.11 |
S3 403 에러 해결방법 (0) | 2024.02.08 |
EC2 서버에 환경변수 추가해 키값 안보이게 설정하기 (0) | 2024.02.04 |
Spring Boot와 Mysql 연결 후 테이블 자동 생성 (0) | 2024.01.06 |