Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.mapstruct:mapstruct:1.5.1.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.google.code.gson:gson'

implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('javadoc') {
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/com/springboot/advice/GlobalExceptionAdvice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.springboot.advice;

import com.springboot.exception.BusinessLogicException;
import com.springboot.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());

return response;
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

return response;
}

@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());

return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode()
.getStatus()));
}

@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {

final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);

return response;
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {

final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
"Required request body is missing");

return response;
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMissingServletRequestParameterException(
MissingServletRequestParameterException e) {

final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
e.getMessage());

return response;
}

@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception e) {
log.error("# handle Exception", e);
// TODO 애플리케이션의 에러는 에러 로그를 로그에 기록하고, 관리자에게 이메일이나 카카오 톡,
// 슬랙 등으로 알려주는 로직이 있는게 좋습니다.

final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);

return response;
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/springboot/audit/Auditable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.springboot.audit;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {

@CreatedDate
@Column(name ="created_at", updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();

@LastModifiedDate
@Column(name = "LAST_MODIFIED_AT")
private LocalDateTime modifiedAt = LocalDateTime.now();
}
9 changes: 9 additions & 0 deletions src/main/java/com/springboot/auth/dto/LoginDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.springboot.auth.dto;

import lombok.Getter;

@Getter
public class LoginDto {
private String username;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.springboot.auth.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.springboot.auth.dto.LoginDto;
import com.springboot.auth.jwt.JwtTokenizer;
import com.springboot.member.entity.Member;
import lombok.SneakyThrows;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenizer jwtTokenizer;


public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) {
this.authenticationManager = authenticationManager;
this.jwtTokenizer = jwtTokenizer;
}

@Override
@SneakyThrows
public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) {
ObjectMapper objectMapper = new ObjectMapper();
LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
return authenticationManager.authenticate(authenticationToken);
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Member member = (Member) authResult.getPrincipal();
String accessToken = delegateAccessToken(member);
String refreshToken = delegateRefreshToken(member);
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh", refreshToken);
// this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}

protected String delegateAccessToken (Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", member.getEmail());
claims.put("roles", member.getRoles());
String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

String accessToken = jwtTokenizer.generateAccessToken(claims, subject,expiration,base64EncodedSecretKey);
return accessToken;
}
protected String delegateRefreshToken (Member member) {
String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
return refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.springboot.auth.filter;

import com.springboot.auth.jwt.JwtTokenizer;
import com.springboot.auth.utils.JwtAuthorityUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class JwtVerificationFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
private final JwtAuthorityUtils jwtAuthorityUtils;

public JwtVerificationFilter(JwtTokenizer jwtTokenizer, JwtAuthorityUtils jwtAuthorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.jwtAuthorityUtils = jwtAuthorityUtils;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 검증된 claims 를 데리고 와서
Map<String, Object> claims = verifyJws(request);
// claims 를 넣고 Context 를 만들어줌.
setAuthenticationToContext(claims);
filterChain.doFilter(request, response);
}

@Override
protected boolean shouldNotFilter (HttpServletRequest request) throws ServletException {
String authorization = request.getHeader("Authorization");
return authorization == null || !authorization.startsWith("Bearer");
}

// 요청 헤더에서 Jws 를 데리고 옴.
// getClaims 로 검증을 함.
private Map<String, Object> verifyJws (HttpServletRequest request) {
String jws = request.getHeader("Authorization").replace("Bearer ", "");
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
// 검증된 claims 를 받음. claims 에 있는 body 를 데리고 오기 때문에 getBody()
Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();
return claims;
}

private void setAuthenticationToContext (Map<String, Object> claims) {
String username = (String) claims.get("username");
// claims 에 있는 Roles 정보를 데리고 와서, Authorities 를 만든다.
List<GrantedAuthority> authorities = jwtAuthorityUtils.createAuthorities((List)claims.get("roles"));
// 이걸 Authentication 객체로 만듦.
Authentication authentication = new UsernamePasswordAuthenticationToken(
username, null, authorities);
//Security Context 에 만든 authentication 을 넣음.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.springboot.auth.handler;

import com.google.gson.Gson;
import com.springboot.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("Authenticated failed");
log.error("Authenticated failed", exception.getMessage());
sendErrorResponse(response);
}

private void sendErrorResponse(HttpServletResponse response) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.springboot.auth.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("Authenticated Success");
}
}
Loading