// 인증, 권한 필터 설정
httpSecurity.authorizeHttpRequests(config -> config
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers(
mvc.pattern("/"),
mvc.pattern("/auth/**")
).permitAll()
.requestMatchers(mvc.pattern("/api/v1/auth/**")).permitAll()
.anyRequest().authenticated());
package com.example.jwtvelog.domain.auth.temp.controller;
import com.example.jwtvelog.auth.jwt.JwtToken;
import com.example.jwtvelog.auth.session.CustomUserDetails;
import com.example.jwtvelog.common.dto.ResDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/temp")
public class TempControllerApiV1 {
@GetMapping
public HttpEntity<?> temp(@AuthenticationPrincipal CustomUserDetails customUserDetails){
// customUserDetails에는 로그인한 유저의 정보가 들어있다.
return new ResponseEntity<>(
ResDTO.builder()
.code(0)
.message("인증 성공")
.data(customUserDetails.getUsername())
.build(),
HttpStatus.OK);
}
}
package com.example.jwtvelog.auth.jwt;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.jwtvelog.auth.session.CustomUserByIdxService;
import com.example.jwtvelog.auth.session.CustomUserDetails;
import com.example.jwtvelog.common.exception.UnauthorizedException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtAuthorizationFilter extends GenericFilterBean {
// idx를 이용해 CustomUserDetails를 가져오는 서비스
private final CustomUserByIdxService customUserByIdxService;
// 생성자
@Autowired
public JwtAuthorizationFilter(CustomUserByIdxService customUserByIdxService) {
this.customUserByIdxService = customUserByIdxService;
}
// 필터 로직 구현
@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain
)
throws IOException, ServletException {
// HttpServletRequest 객체로 서블릿 요청 객체를 캐스팅
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
// HttpServletResponse 객체로 서블릿 응답 객체를 캐스팅
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
// HTTP 요청의 헤더에서 JWT 토큰을 가져온다.
// JwtProvider.HEADER는 토큰을 저장하는 데 사용되는 HTTP 헤더의 이름
String prefixJwt = httpRequest.getHeader(JwtProvider.HEADER);
// 토큰이 없으면 다음 필터로 넘긴다.
if (prefixJwt == null) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 토큰 접두사 "Bearer"을 제거한다.
String jwt = prefixJwt.replace(JwtProvider.TOKEN_PREFIX, "");
try {
// jwtProvider 객체를 생성하고
JwtProvider jwtProvider = new JwtProvider();
// jwtProvider 객체의 verify 메서드를 사용해 토큰을 검증한다.
DecodedJWT decodedJWT = jwtProvider.verify(jwt);
// 토큰에서 idx를 가져온다. (우리가 idx로 설정 해놓은 주제, 즉 subject)
Long idx = Long.parseLong(decodedJWT.getSubject());
// idx를 이용해 CustomUserDetails를 가져온다.
CustomUserDetails customUserDetails = customUserByIdxService.loadUserByIdx(idx);
// 가져온 CustomUserDetails를 사용해
// Authentication에 담길 UsernamePasswordAuthenticationToken 객체를 생성한다.
Authentication authentication = new UsernamePasswordAuthenticationToken(
customUserDetails,
customUserDetails.getPassword(),
customUserDetails.getAuthorities());
// SecurityContext에 Authentication 객체를 저장한다.
// 이로써 Spring Security가 인증된 사용자라고 인식하고,
// 컨트롤러에서 @AuthenticationPrincipal 어노테이션을 사용해 사용자 정보를 가져올 수 있다.
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (UnauthorizedException e) {
// json 형태로 401 응답
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 상태 코드 설정
httpResponse.setContentType("application/json;charset=UTF-8"); // 컨텐츠 타입 설정
// JSON으로 오류 메시지 생성
Map<String, Object> data = new HashMap<>();
data.put("code", -1);
data.put("message", "인증 오류: " + e.getMessage());
data.put("data", "Unauthorized");
data.put("status", HttpServletResponse.SC_UNAUTHORIZED);
// ObjectMapper를 사용해 Map을 JSON으로 변환
String json = new ObjectMapper().writeValueAsString(data);
// 응답에 JSON 쓰기
httpResponse.getWriter().write(json);
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
return;
} finally {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
package com.example.jwtvelog.auth.session;
import com.example.jwtvelog.model.member.entity.MemberEntity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
@Setter
@Getter
public class CustomUserDetails implements UserDetails {
// 유저 정보를 담을 필드
private MemberEntity member;
// 생성자로 MemberEntity를 받아서 CustomUserDetails를 생성한다.
public CustomUserDetails(MemberEntity member) {
this.member = member;
}
// 권한을 가져온다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collector = new ArrayList<>();
collector.add(() -> member.getRole());
return collector;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getEmail();
}
// 계정이 만료되지 않았는지 확인한다.
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겨있지 않은지 확인한다.
@Override
public boolean isAccountNonLocked() {
return true;
}
// 계정의 인증 정보가 만료되지 않았는지 확인한다.
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정이 활성화되어있는지 확인한다.
@Override
public boolean isEnabled() {
return true;
}
}
package com.example.jwtvelog.auth.session;
import com.example.jwtvelog.model.member.entity.MemberEntity;
import com.example.jwtvelog.model.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class CustomUserByIdxService {
// 유저 정보를 찾아오기 위해 MemberRepository를 주입받는다.
private final MemberRepository memberRepository;
public CustomUserDetails loadUserByIdx(Long idx) {
// idx로 유저 정보를 찾아온다.
// 존재하지 않는다면 UsernameNotFoundException을 발생시킨다.
MemberEntity userEntity = memberRepository.findByIdx(idx).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + idx)
);
// 유저 정보를 기반으로 CustomUserDetails를 생성하여 반환한다.
return new CustomUserDetails(userEntity);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/auth/login">로그인</a>
<a href="/auth/sign-up">회원가입</a>
<button id="jwt-test">JWT 테스트</button>
</body>
<script>
document.querySelector("#jwt-test").addEventListener("click", () => {
fetch("/api/v1/temp", {
method: "GET",
headers: {
"Content-Type": "application/json",
// 쿠키에 저장된 토큰을 헤더에 포함시켜 보냄
"Authorization": getCookie("ACCESS-TOKEN")
}
}).then(response => response.json())
.then((result) => {
// result.code가 0이 아닐 시 에러 메시지 출력
if (result.code !== 0) {
alert(result.message);
return;
}
// result.code가 0일 시 성공
console.log(result);
alert(result.message);
}
)
});
// 쿠키에서 토큰을 가져오는 함수
function getCookie(name) {
// 쿠키를 가져옴
const value = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
// 쿠키가 존재할 경우 토큰을 반환
return value ? value[2] : null;
}
</script>
</html>
전
// 엑세스 토큰 유효기간 1일 설정
private static final int EXP_ACCESS = 1000 * 60 * 60 * 24;
// 리프레시 토큰 유효기간 7일 설정
private static final int EXP_REFRESH = 1000 * 60 * 60 * 24 * 7;
후
// 엑세스 토큰 유효기간 30초 설정
private static final int EXP_ACCESS = 1000 * 30;
// 리프레시 토큰 유효기간 60초 설정
private static final int EXP_REFRESH = 1000 * 60;
document.querySelector("#jwt-test").addEventListener("click", () => {
fetch("/api/v1/temp", {
method: "GET",
headers: {
"Content-Type": "application/json",
// 쿠키에 저장된 토큰을 헤더에 포함시켜 보냄
"Authorization": "Bearer " + getCookie("ACCESS-TOKEN")
}
}).then(response => response.json())
.then((result) => {
// result.code가 0이 아닐 시 에러 메시지 출력
if (result.code !== 0) {
// 콘솔 출력 추가
console.log(result);
alert(result.message);
return;
}
// result.code가 0일 시 성공
console.log(result);
alert(result.message);
}
)
});
// 재 로그인 (AccessToken 만료 시 RefreshToken으로 재발급, RefreshToken 만료 시 UnauthorizedException 발생)
@PostMapping("/relogin")
public HttpEntity<?> reLogin(@RequestBody @Valid ReqReLoginApiV1DTO reqReLoginApiV1DTO, Errors error) {
if (error.hasErrors()) {
throw new BadRequestException(error.getAllErrors().get(0).getDefaultMessage());
}
return authServiceApiV1.reLogin(reqReLoginApiV1DTO);
}
@Transactional
public HttpEntity<?> reLogin(ReqReLoginApiV1DTO reqReLoginApiV1DTO) {
// jwt 선언 (try-catch 문에서 선언 시 스코프 바깥에서 사용 불가)
DecodedJWT decodedJwt = null;
try {
// jwtProvider의 verify 함수를 사용하여 token 검증
decodedJwt = jwtProvider.verify(reqReLoginApiV1DTO.getRefreshToken());
} catch (UnauthorizedException e) {
// refreshToken 자체가 만료되었거나, 문제가 있으면 UnauthorizedException 발생
throw new UnauthorizedException(e.getMessage());
}
// 검증 성공 시 토큰의 타입이 RefreshToken 인지 검사
if (!decodedJwt.getClaim("token-type").asString().equals(JwtTokenType.REFRESH_TOKEN.name())) {
// 토큰 타입이 RefreshToken이 아니면 UnauthorizedException 발생
throw new UnauthorizedException("토큰 타입이 잘못되었습니다.");
}
// RefreshToken이며, 검증 성공 시 유저 정보 얻어오기
String accessToken = jwtProvider.createToken(Long.parseLong(decodedJwt.getSubject()),
decodedJwt.getClaim("role").asString(), JwtTokenType.ACCESS_TOKEN);
String refreshToken = jwtProvider.createToken(Long.parseLong(decodedJwt.getSubject()),
decodedJwt.getClaim("role").asString(), JwtTokenType.REFRESH_TOKEN);
// accessToken, refreshToken 을 JwtToken 객체에 담아서 반환
return new ResponseEntity<>(
ResDTO.builder()
.code(0)
.message("refreshToken 재발급 완료")
.data(JwtToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build())
.build(),
HttpStatus.OK);
}
public String createToken(Long idx, String role, JwtTokenType tokenType) {
// 입력된 토큰 타입에 따라 유효기간 설정
int exp = tokenType.compareTo(JwtTokenType.ACCESS_TOKEN) == 0 ? EXP_ACCESS : EXP_REFRESH;
return JWT.create()
.withSubject(idx.toString())
.withExpiresAt(new Date(System.currentTimeMillis() + exp))
.withClaim("role", role)
.withClaim("token-type", tokenType.name())
.sign(Algorithm.HMAC512(SECRET));
}
document.querySelector("#jwt-test").addEventListener("click", () => {
fetch("/api/v1/temp", {
method: "GET",
headers: {
"Content-Type": "application/json",
// 쿠키에 저장된 토큰을 헤더에 포함시켜 보냄
"Authorization": "Bearer " + getCookie("ACCESS-TOKEN")
}
}).then(response => {
// response의 status가 401(JwtProvider의 verify가 실패)일 경우
// 다른 경우일 수도 있다.
if(response.status === 401) {
// 재발급 요청에 필요한 데이터를 담은 DTO
reloginDTO = {
refreshToken: getCookie("REFRESH-TOKEN")
}
// 재발급 요청
fetch("/api/v1/auth/relogin",{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reloginDTO)
}).then(response => {
// 재발급 요청 실패 시 (리프레시 토큰도 만료되었을 경우)
if(response.status === 401) {
// 사용자를 로그인 페이지로 이동시킨다.
alert("로그인이 필요합니다.");
location.href = "/auth/login";
return;
}
// 재발급 요청 성공 시 파싱하여 return
return response.json();
}).then(result => {
// 토큰 만료 이외의 이유로 재발급 요청 실패 시 에러 메시지 출력
if(result.code !== 0) {
alert(result.message);
return;
}
// 토큰 재발급 성공 시 콘솔에 띄워보고 쿠키에 저장
console.log(result);
document.cookie = `ACCESS-TOKEN=${result.data.accessToken}`;
document.cookie = `REFRESH-TOKEN=${result.data.refreshToken}`;
})
}
return response.json()
})
.then((result) => {
// result.code가 0이 아닐 시 에러 메시지 출력
if (result.code !== 0) {
console.log(result);
return;
}
// result.code가 0일 시 성공
console.log(result);
alert(result.message);
}
)
});
// 엑세스 토큰 유효기간 1일 설정
private static final int EXP_ACCESS = 1000 * 60 * 60 * 24;
// 리프레시 토큰 유효기간 7일 설정
private static final int EXP_REFRESH = 1000 * 60 * 60 * 24 * 7;
끝.