작성
·
1.4K
·
수정됨
0
스프링부트 2.7.7 버전에서는 사용할때 아무 문제없던 코드들인데 3.1 버전 업데이트하면서 securityFilterChain 이부분 문법만 살짝 바뀐정도입니다
그런데
jwt 토큰에서 chain.doFilter(request, response); 까지 잘 넘어가고
비즈니스 로직인 service 단에서 에러가 터졌을때 (NullPointException,IllegalArgumentException)
에러가 났을경우
다시 엔트리포인트로 넘어와서 에러처리가 되는데 왜 이럴까요 ??
무슨 에러가 나든
엔트리포인트로 넘어오는데 엔트리포인트는 인증 실패일경우에 실행되어야하는걸로 알고있습니다
토큰값은 모두 유효하고 jwt 토큰 인증은 되었음에도 불구하고 엔트리포인트로 넘어가는 이유를 모르겠습니다
아래 에러는 비즈니스로직에서 에러 난 코드이며
2023-10-30T23:56:16.287+09:00 ERROR 26006 --- [nio-8081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: No enum constant com.tripcoach.core.domain.alarm.enums.AlarmType.COACH] with root cause
java.lang.IllegalArgumentException: No enum constant com.tripcoach.core.domain.alarm.enums.AlarmType.COACH
at java.base/java.lang.Enum.valueOf(Enum.java:273) ~[na:na]
at org.hibernate.type.descriptor.java.EnumJavaType.fromName(EnumJavaType.java:231) ~[hibernate-core-6.2.9.Final.jar:6.2.9.Final]
at org.hibernate.type.descriptor.converter.internal.NamedEnumValueConverter.toDomainValue(NamedEnumValueConverter.java:53) ~[hibernate-core-6.2.9.Final.jar:6.2.9.Final]
바로 에러코드 다음이
2023-10-30T23:56:16.291+09:00 ERROR 26006 --- [nio-8081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "exception" is null
at org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.resolveMethodByThrowable(ExceptionHandlerMethodResolver.java:146) ~[spring-web-6.0.12.jar:6.0.12]
at org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.resolveMethod(ExceptionHandlerMethodResolver.java:134) ~[spring-web-6.0.12.jar:6.0.12]
엔트리포인트에서 넘어와버리네요
부족한 코드이지만 아무리 찾아도 모르겠어서 글 남깁니다.
아래 전체코드 첨부합니다
답변 2
0
import com.tripcoach.app_api.api.member.repository.MemberRepository;
import com.tripcoach.app_api.security.token.CustomAuthenticationToken;
import com.tripcoach.core.domain.member.entity.Member;
import com.tripcoach.core.domain.member.enums.OsType;
import com.tripcoach.core.domain.member.enums.SnsType;
import com.tripcoach.core.common.exception.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.transaction.annotation.Transactional;
import static com.tripcoach.core.common.apiresult.comcode.ComCode.LOGIN_FAIL;
public class CustomAuthenticationManager implements AuthenticationManager {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private MemberRepository memberRepository;
@Override
@Transactional
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CustomAuthenticationToken customAuthenticationToken = (CustomAuthenticationToken) authentication;
System.out.println("customAuthenticationToken.getSnsType() = " + customAuthenticationToken.getSnsType());
String snsId = customAuthenticationToken.getSnsId();
SnsType snsType = SnsType.valueOf(customAuthenticationToken.getSnsType());
String fcmToken = customAuthenticationToken.getFcmToken();
OsType osType = OsType.valueOf(customAuthenticationToken.getOsType());
Member member = memberRepository.findBySnsIdAndSnsType(snsId, snsType)
.orElseThrow(() -> new BadCredentialsException(LOGIN_FAIL));
member.updateFcmToken(fcmToken);
member.updateOsType(osType);
UserDetails userDetails = userDetailsService.loadUserByUsername(String.valueOf(member.getIdx()));
if (userDetails == null) {
throw new BusinessException("로그인에 실패 했습니다.");
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
import com.tripcoach.core.common.exception.exception.BusinessException;
import com.tripcoach.core.domain.member.entity.Member;
import io.jsonwebtoken.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import static com.tripcoach.app_api.security.config.SecurityCode.*;
import static java.util.stream.Collectors.toList;
@Slf4j
@Component
public class JwtTokenProvider implements Serializable {
private static final String SECRET_KEY = "";
private static final long EXPIRATION_TIME = 1000 * 1800; // 30분초
// private static final long EXPIRATION_TIME = 30 * 24 * 60 * 60 * 1000L; // 1달
// private static final long EXPIRATION_TIME = 300 * 600 * 100L; //300분
/**
* Jwt 생성
*/
public String generateToken(Member member) {
ArrayList<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority(member.getGrade().label()));
Date now = new Date();
Date expiration = new Date(now.getTime() + EXPIRATION_TIME);
return Jwts.builder()
.setSubject(member.getIdx().toString())
.claim("authorities", roles.stream().map(GrantedAuthority::getAuthority).collect(toList()))
.claim("nickname", member.getNickname()) // 이 부분을 추가하여 nickname 클레임을 설정합니다
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
// /**
// * JWT 회원 정보 추출.
// */
public Claims getUsernameFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new ExpiredJwtException(null, null, "토큰이 만료 되었습니다");
} catch (MalformedJwtException e) {
throw new BusinessException("잘못된 토큰입니다."); // 토큰이 잘못된 경우 처리할 내용
}
}
public Claims getUsernameFromToken(String token, HttpServletRequest request) {
Claims claims = null;
try {
claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
request.setAttribute(JWT_EXCEPTION.label(), EXPIRED_JWT_EXCEPTION);
} catch (MalformedJwtException e) {
request.setAttribute(JWT_EXCEPTION.label(), MALFORMED_JWT_EXCEPTION);
} catch (UnsupportedJwtException e) {
request.setAttribute(JWT_EXCEPTION.label(), UNSUPPORTED_JWT_EXCEPTION);
} catch (SignatureException e) {
request.setAttribute(JWT_EXCEPTION.label(), SIGNATURE_EXCEPTION);
} catch (IllegalArgumentException e) {
request.setAttribute(JWT_EXCEPTION.label(), ILLEGAL_ARGUMENT_EXCEPTION);
}catch (NullPointerException e) {
request.setAttribute(JWT_EXCEPTION.label(), NULL_POINT_EXCEPTION);
}
return claims;
}
}
import com.tripcoach.app_api.api.member.repository.MemberRepository;
import com.tripcoach.core.domain.member.entity.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@RequiredArgsConstructor
@Service("userDetailService")
public class CustomUserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String idx) throws UsernameNotFoundException {
Member member = memberRepository.findById(Long.valueOf(idx))
.orElseThrow(() -> new UsernameNotFoundException("User not found with idx: " + idx));
ArrayList<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority(member.getGrade().label()));
return new MemberContext(member, roles);
}
}
import com.tripcoach.core.domain.member.entity.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class MemberContext extends User {
private final Member member;
public MemberContext(Member member, Collection<? extends GrantedAuthority> authorities) {
super(String.valueOf(member.getIdx()), "beeb.tripcoach", authorities);
this.member = member;
}
public Member getMember() {
return member;
}
public Long getId() {
return member.getIdx();
}
}
import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
@Getter
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private final String snsId;
private final String snsType;
private final String fcmToken;
private final String osType;
public CustomAuthenticationToken(String snsId, String snsType, String fcmToken, String osType) {
super(null);
this.snsId = snsId;
this.snsType = snsType;
this.fcmToken = fcmToken;
this.osType = osType;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return snsId;
}
}
0
import com.tripcoach.app_api.security.exception.JwtAuthenticationEntryPoint;
import com.tripcoach.app_api.security.filter.CustomAuthenticationFilter;
import com.tripcoach.app_api.security.filter.JwtAuthenticationFilter;
import com.tripcoach.app_api.security.handler.CustomAccessDeniedHandler;
import com.tripcoach.app_api.security.handler.CustomAuthenticationFailureHandler;
import com.tripcoach.app_api.security.handler.CustomAuthenticationSuccessHandler;
import com.tripcoach.app_api.security.provider.CustomAuthenticationManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import static com.tripcoach.core.common.apiresult.comcode.ComCode.PERMIT_ALL;
import static org.springframework.security.config.Customizer.withDefaults;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(PERMIT_ALL).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandling ->
exceptionHandling
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 커스텀 인증 필터
@Bean
public CustomAuthenticationFilter customAuthenticationProcessingFilter() {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationManager(customAuthenticationManager());
filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
return filter;
}
// 커스텀 인증 매니저
@Bean
public CustomAuthenticationManager customAuthenticationManager() {
return new CustomAuthenticationManager();
}
}
import com.fasterxml.jackson.annotation.JsonCreator;
import com.tripcoach.core.common.enumType.Constant;
import lombok.Data;
import lombok.Getter;
import java.util.Arrays;
public enum SecurityCode implements Constant {
JWT_EXCEPTION("JWT_EXCEPTION", "JWT TOKEN 만료 되었거나 잘못 되었습니다."),
EXPIRED_JWT_EXCEPTION("ExpiredJwtException", "토큰이 만료 되었습니다.\n토큰을 재발급 해주세요"),
MALFORMED_JWT_EXCEPTION("MalformedJwtException", "손상된 토큰입니다.\n관리자에게 문의해주세요"),
UNSUPPORTED_JWT_EXCEPTION("UnsupportedJwtException", "지원하지 않은 토큰 입니다."),
SIGNATURE_EXCEPTION("SignatureException", "시그니처 검증에 실패한 토큰 입니다."),
ILLEGAL_ARGUMENT_EXCEPTION("IllegalArgumentException", "관리자에게 문의해주세요"),
NULL_POINT_EXCEPTION("NullPointException", "빈값 입니다");
private final String label;
private final String message;
SecurityCode(String label, String message) {
this.label = label;
this.message = message;
}
public String label() {
return label;
}
public String message() {
return message;
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tripcoach.app_api.security.config.SecurityCode;
import com.tripcoach.core.common.apiresult.dto.ApiResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
import static com.tripcoach.app_api.security.config.SecurityCode.*;
import static com.tripcoach.core.common.apiresult.util.ApiUtils.error;
import static jakarta.servlet.http.HttpServletResponse.*;
import static org.apache.commons.codec.CharEncoding.UTF_8;
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
SecurityCode jwtException = (SecurityCode) request.getAttribute(JWT_EXCEPTION.label());
// String errorMessage = getErrorMessage(jwtException);
String errorMessage = "관리자에게 문의해주세요";
int errorCode = 400;
if (jwtException != null) {
switch (jwtException) {
case EXPIRED_JWT_EXCEPTION:
errorMessage = EXPIRED_JWT_EXCEPTION.message();
errorCode = 401;
break;
case MALFORMED_JWT_EXCEPTION:
errorMessage = MALFORMED_JWT_EXCEPTION.message();
errorCode = 401;
break;
case UNSUPPORTED_JWT_EXCEPTION:
errorMessage = UNSUPPORTED_JWT_EXCEPTION.message();
errorCode = 401;
break;
case SIGNATURE_EXCEPTION:
errorMessage = SIGNATURE_EXCEPTION.message();
errorCode = 401;
break;
case ILLEGAL_ARGUMENT_EXCEPTION:
errorMessage = ILLEGAL_ARGUMENT_EXCEPTION.message();
errorCode = 400;
break;
case NULL_POINT_EXCEPTION:
errorMessage = NULL_POINT_EXCEPTION.message();
errorCode = 400;
break;
default:
errorMessage = JWT_EXCEPTION.message(); // 기본적으로 설정할 메시지
errorCode = 500;
break;
}
}
response.setContentType("application/json");
response.setCharacterEncoding(UTF_8);
response.setStatus(errorCode);
ApiResult<?> error = error(request.getServletPath(), errorMessage, errorCode);
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(error);
PrintWriter out = response.getWriter();
out.print(jsonResponse);
out.flush();
}
}
import com.tripcoach.app_api.security.token.CustomAuthenticationToken;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public CustomAuthenticationFilter() {
super(new AntPathRequestMatcher("/login"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String snsId = request.getParameter("snsId");
String snsType = request.getParameter("snsType");
String fcmToken = request.getParameter("fcmToken");
String osType = request.getParameter("osType");
CustomAuthenticationToken customToken = new CustomAuthenticationToken(snsId, snsType, fcmToken, osType);
return getAuthenticationManager().authenticate(customToken);
}
}
import com.tripcoach.app_api.security.provider.JwtTokenProvider;
import com.tripcoach.app_api.security.service.CustomUserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private final CustomUserDetailsServiceImpl userDetailsService;
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader(AUTHORIZATION_HEADER);
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith(BEARER_PREFIX)) {
try {
jwtToken = requestTokenHeader.substring(7);
Claims claims = jwtTokenProvider.getUsernameFromToken(jwtToken, request);
username = claims.getSubject();
String nickname = (String) claims.get("nickname");
log.info("유저 인덱스 = {} 유저 닉네임 = {} ", username, nickname);
} catch (NullPointerException e) {
log.info("로그인 실패");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 토큰이 유효한 경우.. 수동으로 인증을 설정하도록 Spring Security를 구성
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 컨텍스트에서 인증을 설정한 후, 현재 사용자가 인증되었음을 지정. Spring Security Configurations 성공적으로 통과.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
chain.doFilter(request, response);
}
}
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 403 권한 거부 에러를 뱉는다.
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tripcoach.core.common.apiresult.dto.ApiResult;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
import java.io.PrintWriter;
import static com.tripcoach.core.common.apiresult.comcode.ComCode.AUTH_FAIL_CODE;
import static com.tripcoach.core.common.apiresult.comcode.ComCode.LOGIN_FAIL;
import static com.tripcoach.core.common.apiresult.util.ApiUtils.error;
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ApiResult<?> error = error(null, LOGIN_FAIL, AUTH_FAIL_CODE);
// Convert the DTO to a JSON string
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(error);
// Set the content type of the response to JSON
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// Write the JSON string to the response body
PrintWriter out = response.getWriter();
out.print(jsonResponse);
out.flush();
}
}
@Slf4j
@RequiredArgsConstructor
@Service
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final FlyingDistanceLogService flyingDistanceLogService;
@Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
Member member = ((MemberContext) authentication.getPrincipal()).getMember();
String token = new JwtTokenProvider().generateToken(member);
LoginRespDto result = new LoginRespDto(member, token);
ApiResult<LoginRespDto> success = success(result, SUCCESS, SUCCESS_CODE);
// Convert the DTO to a JSON string
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(success);
// Set the content type of the response to JSON
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// Write the JSON string to the response body
PrintWriter out = response.getWriter();
out.print(jsonResponse);
out.flush();
// 로그인 성공했을때만 이력을 남김
flyingDistanceLogService.loginLog(member);
}
}
전체 실행 코드 공유 부탁드립니다