1단계
로그인
2단계
Post (feat.POSTMAN)
JSON
(auth/login)
3단계
AuthController
@RequestBody
Dto
{
username
password
}
4단계
AuthService <- AuthenticationManager
SpringBoot Security -> IOC(Inversion of Control) -> amb
JWT 토큰 return -> AuthController -> 응답 ResponseEntity, DataResponse(JWT) -> 로그인
-> 최종적으로 Authentication 객체가 return
인터넷에서 데이터를 안전하게 전송하기 위한 토큰 기반 인증 방식 중 하나
JWT는 JSON 객체를 사용하여 정보를 안전하게 전달
JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 이루어져 있으면, 각 부분은 .으로 구분
Header는 JWT의 종류와 사용된 암호화 알고리즘 정보를 담고 있음Payload는 JWT에 담을 정보를 JSON 객체로 표현한 부분으로, 사용자 정보와 같은 클레임(Claim)을 포함할 수 있음Signature 은 Token이 변조되었는지 검증하기 위한 부분JWT 는 발급된 이후에는 수정이 불가능하며, JWT를 사용하여 인증할 경우, 서버 측에서는 클라이언트가 보낸 JWT를 검증하기만 하면 됨
이러한 특징으로 인해 JWT는 분산 시스템에서 인증에 활용되는 것이 효과적임
- 흐름구조
Client -> Filter -> Controller
SecurityConfig (수정)
package com.web.study.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.web.study.security.jwt.JwtAuthenticationFilter;
import com.web.study.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// security filter
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.httpBasic().disable(); // 웹 기본 인증 방식
http.formLogin().disable(); // 폼태그 통한 로그인
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션 비활성 (무상태성)
http.authorizeRequests()
.antMatchers("/auth/register/**", "/auth/login/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}
}
인증은
security filter단에서 이루어져야한다.

null에 들어갈Filter가 필요하다
package com.web.study.security.jwt;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
//DI (의존성 주입) JwtAuthenticationFilter가 생성될 때 jwtTokenProvider가 만들어짐
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = getToken(request);
boolean validationFlag = jwtTokenProvider.validateToken(token);
if(validationFlag ) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String getToken(ServletRequest request) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String type = "Bearer";
String token = httpServletRequest.getHeader("Authorization");
// hasText 문자열이 Null 또는 공백이 아닌지 확인
if(StringUtils.hasText(token) && token.startsWith(type)) {
return token.substring(type.length() + 1);
}
return null;
}
}
JwtTokenProvider (수정)
package com.web.study.security.jwt;
import java.security.Key;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.web.study.dto.response.auth.JwtTokenRespDto;
import com.web.study.exception.CustomException;
import com.web.study.security.PrincipalUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public JwtTokenRespDto creatToken(Authentication authentication) {
StringBuilder authoritiesBuilder = new StringBuilder();
authentication.getAuthorities().forEach(grantedcAuthority -> {
authoritiesBuilder.append(grantedcAuthority.getAuthority());
authoritiesBuilder.append(",");
});
authoritiesBuilder.delete(authoritiesBuilder.length() - 1, authoritiesBuilder.length());
String authorities = authoritiesBuilder.toString();
long now = (new Date()).getTime();
// 1000 == 1초
Date tokenExpiresDate = new Date(now + (1000 * 60 * 300)); // 토큰 만료 시간 ( 현재 시간 + Timer)
PrincipalUserDetails userDetails = (PrincipalUserDetails) authentication.getPrincipal();
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim("userId", userDetails.getUserId())
.claim("auth", authorities)
.setExpiration(tokenExpiresDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return JwtTokenRespDto.builder()
.grantType("Bearer")
.accessToken(accessToken)
.build();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
}catch(SecurityException | MalformedJwtException e) {
// Security 라이브러리에 오유가 있거나, JSON의 포맷이 잘못된 형식의 JWT가 들어왔을 때 예외
// SignatureException이 포함되어 있음
log.info("Invalid JWT Token", e);
}catch (ExpiredJwtException e) {
// 토큰의 유효기간이 만료된 경우의 예외
log.info("Expired JWT Token", e);
}catch (UnsupportedJwtException e) {
// jwt의 형식을 지키지 않은 경우 (Header.Payload.Signature)
log.info("Unsupported JWT Token", e);
}catch (IllegalArgumentException e) {
// jwt 토큰이 없을때
log.info("IllegalArgument JWT Token", e);
}catch (Exception e) {
log.info("JWT Token", e);
}
return false;
}
public Authentication getAuthentication(String accessToken){
Claims claims = parseClaims(accessToken);
Object roles = claims.get("auth");
if(roles == null) {
throw new CustomException("권한 정보가 없는 토큰입니다.");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
String[] rolesArray = roles.toString().split(",");
Arrays.asList(rolesArray).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UserDetails userDetails = new User(claims.getSubject(),"",authorities);
return new UsernamePasswordAuthenticationToken(userDetails,"",authorities);
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
}catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
POSTMAN SEND


JwtAuthenticationEntryPoint 생성
package com.web.study.security.jwt;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.web.study.dto.ErrorResponseDto;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
ObjectMapper responseJson = new ObjectMapper();
out.println(responseJson.writeValueAsString(ErrorResponseDto.of(HttpStatus.UNAUTHORIZED,authException)));
}
}
Token 등록

- 회원가입할 정보를 입력
- 해당 정보로 회원가입을 요청
- 회원가입 정보(password는 암호화)를 DB에 저장
> AuthController 회원가입 정보를 post요청(RegisteUserReqDto)
> toEntity에서 password 암호화 진행
> 회원가입 등록 완료 후, 예외가 있는게 아니면 Controller에서 return
- 로그인할 정보를 입력 (username, password)
- 해당 정보로 로그인 요청
- AuthenticationManager에게 username, password를 전달
- AuthenticationManager가 인증을 시작
- UserDetailsService의 loadUserByUsername(String username)이 호출
> userDetails를 리턴받아서 Authentication객체를 생성하기 위함
> 이때, 해당 username으로 DB에서 조회된 UserEntity가 없으면 등록되지 않은 회원 (예외처리)
> Authentication객체가 생성되면 로그인 성공
- Authentication객체를 JWT로 변환하는 작업을 수행
- 변환된 JWT를 클라이언트에게 응답
- 클라이언트는 JWT토큰을 로컬스토리지나 쿠키에 저장
- 요청 Header에 Bearer 방식으로 JWT 토큰을 전달
- Spring Security에서 인증이 필요한 요청들은 JwtAuthenticationFilter를 통해 인증절차를 진행
> 이때, 인증의 최종 목표는 SecurityContextHolder객체의 Context에 Authentication을 등록하는 것
> Authentication 객체가 등록이 되어 있으면 인증이 됨
- JwtAuthenticationFilter에서 요청 Header에 들어있는 Authorization의 JWT토큰을 추출
- JWT 토큰을 검증
> 이때 검증에 실패하여 Exception이 생성되면 AuthenticationEntryPoint가 실행되면서 401 응답을 하게됨
- JWT 토큰 검증이 완료되면 JWT 토큰에서 Claims를 추출
- Claims에서 username과 Authorities를 추출하여 Authentication 객체를 생성
- 생성된 Authentication객체를 SecurityContext에 등록
- 등록이 완료되면 다음 doChain이 호출