

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
LoginRequest loginRequest = new ObjectMapper().readValue(request.getReader(), LoginRequest.class);
LoginAuthentication unauthenticated = LoginAuthentication.unauthenticated(loginRequest);
return this.getAuthenticationManager().authenticate(unauthenticated);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String idAndSocialType = AuthenticationUtil.toIdAndSocialType(authentication.getPrincipal());
MemberDetails memberDetails = (MemberDetails) service.loadUserByUsername(idAndSocialType);
String credentials = (String) authentication.getCredentials();
if (!passwordEncoder.matches(credentials, memberDetails.getPassword())) {
throw new BadCredentialsException("Password Not Matches!");
}
return LoginAuthentication.authenticated(Payload.of(memberDetails), List.of());
}
@Transactional(readOnly = true)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String id = username.split("\\^")[0];
SocialType type = SocialType.valueOf(username.split("\\^")[1]);
return MemberDetails.of(
repository.findByUserIdAndSocialType(id, type).orElseThrow(
() -> new UsernameNotFoundException(id + " Not Found")
)
);
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
Payload payload = TokenUtil.getPayload();
String accessToken = jwtProvider.createToken(payload);
String refreshToken = jwtProvider.refreshToken(payload.getRedisKey());
Jwt jwt = new Jwt(accessToken, refreshToken);
redisService.accessTokenSave(payload.getRedisKey(), jwt.getAccessToken());
redisService.refreshTokenSave(payload.getRedisKey(), jwt.getRefreshToken());
int refreshTokenMaxAge = (int) jwtProvider.getRefreshExpirationTime() / 1000;
Cookie refreshTokenCookie = CookieUtil.createCookie(
jwtProvider.getRefreshTokenHeader(),
URLEncoder.encode(refreshToken, StandardCharsets.UTF_8),
refreshTokenMaxAge,
true,
false,
"/"
);
CookieUtil.addCookie(response, refreshTokenCookie);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setHeader(jwtProvider.getAccessTokenHeader(), jwtProvider.getTokenPrefix() + jwt.getAccessToken());
response.setHeader(jwtProvider.getRefreshTokenHeader(), jwtProvider.getTokenPrefix() + jwt.getRefreshToken());
LoginResponse loginResponse = new LoginResponse(jwt, payload.getId());
PrintWriter writer = response.getWriter();
writer.println(new ObjectMapper().writeValueAsString(loginResponse));
writer.flush();
writer.close();
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write(exception.getMessage());
writer.flush();
writer.close();
}
Access-Token : Local Variables
Refresh-Token : HttpOnly Cookie
Access-Token 은 브라우저 로컬 변수에 저장하고 Refresh-Token 은 HttpOnly Cookie에 저장한다.
이렇게 하면 XSS, CSRF 모두 방어가 가능하다.
const {response, error, fetchData} = useApi('/login', 'post');
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const apiData = {
userId: formData.get('id'),
password: formData.get('password'),
socialType: 'NONE'
};
//로그인 요청
fetchData({method: 'post', data: apiData});
};
useEffect(() => {
//로그인 요청이 정상이면 메인페이지로 이동
if (response && response.status === 200) {
axiosInstance.defaults.headers.common['Authorization'] = response.headers.authorization;
localStorage.setItem("id", response.data.id);
setLoginSuccess(true);
setAuthChecked(true);
nav("/", {replace: true});
return;
}
//로그인 요청 오류 메시지
if (error) {
alert("[" + error.response.status + "] " + error.response.data);
return;
}
}, [response, error]);
클라이언트에서 Refresh-Token URL을 통해 새로운 access-token 을 발급해서 로그인 연장 기능 제공
const useApi = (defaultUrl, defaultMethod = 'get', defaultOptions = {}) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const nav = useNavigate();
const refreshToken = async () => {
try {
const response = await axiosInstance.post('/auth/refresh-token');
const accessToken = response.headers.authorization;
axiosInstance.defaults.headers.common["Authorization"] = accessToken;
localStorage.setItem("id", response.data.id);
} catch (err) {
localStorage.removeItem("id");
alert("로그인 유효시간이 지났습니다. 재로그인 바랍니다.");
nav("/signin", {replace: true});
throw err;
}
};
const fetchData = async ({
url = defaultUrl,
method = defaultMethod,
data = null,
headers = {},
params = {}
} = {}) => {
setLoading(true);
setError(null);
setResponse(null);
try {
const response = await axiosInstance({
url,
method,
data,
headers,
params,
...defaultOptions,
});
setResponse(response);
} catch (err) {
if (err.response && err.response.status === 401 && err.response.data
=== 'Access-Token is expired') {
try {
await refreshToken();
const retryResponse = await axiosInstance({
url,
method,
data,
headers,
params,
...defaultOptions,
});
setResponse(retryResponse);
} catch (error) {
setError(error)
}
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
return {response, error, loading, fetchData};
};
useEffect(() => {
const checkAuthStatus = async () => {
let id = localStorage.getItem('id');
const accessToken = axiosInstance.defaults.headers.common['Authorization'];
if (id && !accessToken) { // id는 있지만, accessToken이 설정되지 않았을 때
try {
const response = await axiosInstance.post('/auth/refresh-token');
axiosInstance.defaults.headers.common["Authorization"] = response.headers.authorization; // 새로운 access-token
localStorage.setItem('id', response.data.id);
setLoginSuccess(true);
} catch (error) {
console.error('Token refresh failed:', error);
localStorage.removeItem("id");
alert("로그인 유효시간이 지났습니다. 재로그인 바랍니다.");
nav("/signin", {replace: true});
return;
}
}
setAuthChecked(true); // 인증이 끝나면 authChecked를 true로 설정
};
checkAuthStatus(); // 컴포넌트가 마운트될 때 체크
}, []);
@PostMapping("/refresh-token")
public void refreshToken(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//1. validate refresh token
String refreshToken = CookieUtil.getCookieValue(request, jwtProvider.getRefreshTokenHeader());
String requestRefreshToken = jwtProvider.resolveToken(refreshToken);
String redisKey = jwtProvider.verifyRefreshToken(requestRefreshToken);
String findRefreshToken = redisService.refreshTokenGet(redisKey);
if (!requestRefreshToken.equals(findRefreshToken)) {
throw new JwtException("Invalid refresh token");
}
//2. find Member
Long memberId = Long.parseLong(redisKey.split("/")[0]); //get MemberId
Member member = memberRepository.findById(memberId)
.orElseThrow();
//3. create payload
Payload payload = Payload.of(MemberDetails.of(member));
//4. set new token and refresh token to response header
LoginAuthentication refreshAuthentication = LoginAuthentication.authenticated(payload, List.of());
SecurityContextHolder.getContext().setAuthentication(refreshAuthentication);
new LoginSuccessHandler(redisService, jwtProvider).onAuthenticationSuccess(request, response, refreshAuthentication);
}
front-end : https://github.com/onlydev7777/emotion-diary-react
back-end : https://github.com/onlydev7777/emotion-diary-monolithic