기능 구현
스프링 시큐리티를 활용해서 jwt 인증 인가를 구현했다.
/api/login 으로 로그인 시도를 하면 JwtAuthenticationFilter에서 로그인 성공 시 jwt 토큰을 반환해준다.
토큰은 리프레쉬가 가능하게 했다.
액세스 토큰은 헤더로 넘겨주고, 리프레쉬 토큰은 쿠키에 저장 후 따로 DB로 관리해주었다.
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
log.info("로그인 성공 및 JWT 생성");
UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal();
String userEmail = userDetails.getUsername();
UserRole role = userDetails.getUser()
.getRole();
String accessToken = jwtUtil.createToken(TokenType.ACCESS, userEmail , role);
String refreshToken = jwtUtil.createToken(TokenType.REFRESH, userEmail , role);
// Refresh 레포지토리에 저장
// 새로 발급한 토큰에 prefix를 제거 해준 후 저장
addRefreshEntity(userEmail, jwtUtil.substringToken(refreshToken));
jwtUtil.addTokenToHeader(response, accessToken);
jwtUtil.addCookie(response, TokenType.REFRESH, refreshToken);
response.setStatus(HttpStatus.OK.value());
}
검증과 인가를 담당하는 필터에서는 토큰의 검증이 이루어진다.
- 액세스 토큰이 있는지
- 토큰 만료되었는지
- 토큰이 access 토큰인지
모든 검증을 마치면 인증객체를 생성해주고 인증처리 해주었다.
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain filterChain) throws ServletException, IOException {
log.info("JwtAuthorizationFilter");
// 헤더 검증
String accessToken = jwtUtil.getAccessTokenFromRequestHeader(req);
if(accessToken == null) {
// 회원가입 안돼있을 때 넘겨줌
filterChain.doFilter(req, res);
return;
}
accessToken = jwtUtil.substringToken(accessToken);
log.info(accessToken);
// 토큰 만료 검증
try{
jwtUtil.isExpired(accessToken);
} catch(ExpiredJwtException e) {
// response body
PrintWriter writer = res.getWriter();
writer.println("access token expired");
// response status code
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// 토큰이 access인지 확인(발급시 페이로드에 명시)
String category = jwtUtil.getCategory(accessToken);
if (!category.equals(TokenType.ACCESS.name())) {
// reponse body
PrintWriter writer = res.getWriter();
writer.println("invalid access token");
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String userEmail = jwtUtil.getUserEmail(accessToken);
Authentication authentication = createAuthentication(userEmail);
setAuthentication(authentication);
filterChain.doFilter(req, res);
}
// 인증 처리
public void setAuthentication(Authentication authentication) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String userEmail) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
return new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
}
액세스 토큰이 만료되었을 때 리프레쉬를 할 수 있게 재발행 관련 컨트롤러를 만들었다.
@PostMapping("/api/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
String refreshToken = null;
Cookie oldCookie = null;
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(TokenType.REFRESH.name())) {
oldCookie= cookie;
refreshToken = cookie.getValue();
break;
}
}
if(refreshToken == null) {
return ResponseEntity.badRequest().body("refresh token null");
}
// decode 해준 후 prefix 제거
refreshToken = jwtUtil.substringToken(jwtUtil.getDecodeToken(refreshToken));
// Refresh token 만료 검증
try{
jwtUtil.isExpired(refreshToken);
} catch (ExpiredJwtException e) {
return ResponseEntity.badRequest().body("refresh token expired");
}
// 토큰이 refresh인지 체크
String categry = jwtUtil.getCategory(refreshToken);
if (!categry.equals(TokenType.REFRESH.name())) {
return ResponseEntity.badRequest().body("refresh token invalid");
}
// DB에 저장되어 있는지 확인
boolean isExist = refreshRepository.existsByRefresh(refreshToken);
if (!isExist) {
return ResponseEntity.badRequest().body("refresh token invalid");
}
String userEmail = jwtUtil.getUserEmail(refreshToken);
String role = jwtUtil.getRole(refreshToken);
// 새 토큰 발급
String newAccessToken = jwtUtil.createToken(TokenType.ACCESS, userEmail, UserRole.valueOf(role));
String newRefreshToken = jwtUtil.createToken(TokenType.REFRESH, userEmail, UserRole.valueOf(role));
// Refresh 토큰 저장소에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
refreshRepository.deleteByRefresh(refreshToken);
// 새로 발급한 토큰에 prefix를 제거 해준 후 저장
addRefreshEntity(userEmail, jwtUtil.substringToken(newRefreshToken));
oldCookie.setMaxAge(0);
response.setHeader(JwtUtil.AUTHORIZATION_HEADER, newAccessToken);
response.addCookie(jwtUtil.createCookie(TokenType.REFRESH, newRefreshToken));
return ResponseEntity.ok().build();
}
리프레쉬 토큰인지 확인과 DB에 저장되어 있는지 확인해준다. 둘다 통과되었다면 기존에 db에 저장되어있던 리프레쉬 토큰을 삭제해주고, 쿠키에 있는 리프레쉬 토큰에는 새로 발급한 리프레쉬 토큰을 넣어주고 헤더에도 새로 발급한 액세스 토큰을 넣어준다.
로그아웃 기능은 GenericFilterBean을 상속받아서 구현했다.
doFilter로 들어왔을 때 uri가 /api/logout인지 검사, POST로 요청했는지 검사해준다. 모든 검증이 끝나면 쿠키에서 리프레쉬 토큰을 가져온다. 가져온 토큰도 검증을 해준다. 검증을 마치면 DB에 있는 리프레쉬 토큰을 삭제하고 쿠키에 있는 리프레쉬 토큰을 없애준다.
@Slf4j(topic = "Logout")
public class JwtLogoutFilter extends GenericFilterBean {
private final JwtUtil jwtUtil;
private final RefreshRepository refreshRepository;
public JwtLogoutFilter(JwtUtil jwtUtil, RefreshRepository refreshRepository) {
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
doFilter((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, filterChain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String requestURI = request.getRequestURI();
// 모든 요청이 들어오기 때문에 logout 요청인지 확인
// ----------------------------------------
if (!requestURI.matches("^/api/logout$")) {
filterChain.doFilter(request, response);
return;
}
String requestMethod = request.getMethod();
if(!requestMethod.equals("POST")){
filterChain.doFilter(request, response);
return;
}
// ----------------------------------------
// 리프레쉬 토큰 쿠키에서 가져오기
String refreshToken = null;
for(Cookie cookie : request.getCookies()){
if(cookie.getName().equals(TokenType.REFRESH.name())){
refreshToken = cookie.getValue();
break;
}
}
refreshToken = jwtUtil.getDecodeToken(refreshToken);
refreshToken = jwtUtil.substringToken(refreshToken);
// 토큰 없으면
if(refreshToken == null){
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 토큰 만료 검사
try{
jwtUtil.isExpired(refreshToken);
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 토큰이 REFRESH인지 검사
String category = jwtUtil.getCategory(refreshToken);
if(!category.equals(TokenType.REFRESH.name())){
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// DB 저장되어있는지 검사
boolean isExist = refreshRepository.existsByRefresh(refreshToken);
if (!isExist) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 로그아웃 진행
refreshRepository.deleteByRefresh(refreshToken);
// Refresh 토큰 Cookie값 없애기
Cookie cookie = new Cookie(TokenType.REFRESH.name(), null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
response.setStatus(HttpServletResponse.SC_OK);
}
}
모든 구현을 마치고 이제 컨트롤러에서
@AuthenticationPrincipal UserDetailsImpl userDetails
매개변수에 위와 같이 넣어주면 userDetails.getUser()로 로그인중인 유저를 가져올 수 있다!!
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRole role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.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;
}
}
'부트캠프 > Dev' 카테고리의 다른 글
뉴스피드 프로젝트 4일차 (1) | 2024.09.05 |
---|---|
뉴스피드 프로젝트 3일차 (0) | 2024.09.05 |
뉴스피드 프로젝트 1일차 (0) | 2024.09.04 |
필터에서 예외처리 (0) | 2024.08.20 |
개인과제 구현 -2 (0) | 2024.08.16 |