-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: jwt 기본 설정 추가 * feat: security config 설정 추가 * feat: userId 관련 로직 추가
- Loading branch information
Showing
11 changed files
with
420 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
src/main/java/site/sonisori/sonisori/auth/cookie/CookieUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package site.sonisori.sonisori.auth.cookie; | ||
|
||
import java.util.Arrays; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.ResponseCookie; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
|
||
public class CookieUtil { | ||
@Value("${spring.jwt.refresh-expiration}") | ||
private final long refreshExpiration; | ||
|
||
public CookieUtil(long refreshExpiration) { | ||
this.refreshExpiration = refreshExpiration; | ||
} | ||
|
||
public ResponseCookie createCookie(String cookieName, String cookieValue, String domain) { | ||
return ResponseCookie.from(cookieName, cookieValue) | ||
.domain(domain) | ||
.path("/") | ||
.httpOnly(true) | ||
.secure(true) | ||
.sameSite("None") | ||
.maxAge(refreshExpiration) | ||
.build(); | ||
} | ||
|
||
public String getCookieValue(HttpServletRequest request, String cookieName) { | ||
if (request == null || cookieName == null) { | ||
throw new IllegalArgumentException(); | ||
} | ||
Cookie[] cookies = request.getCookies(); | ||
if (cookies == null) { | ||
return null; | ||
} | ||
|
||
return Arrays.stream(cookies) | ||
.filter(cookie -> cookieName.equals(cookie.getName())) | ||
.map(Cookie::getValue) | ||
.findFirst() | ||
.orElse(null); | ||
} | ||
|
||
public ResponseCookie clearCookie(String cookieName, String domain) { | ||
return ResponseCookie.from(cookieName, "") | ||
.domain(domain) | ||
.path("/") | ||
.httpOnly(true) | ||
.secure(true) | ||
.sameSite("None") | ||
.maxAge(0) | ||
.build(); | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
src/main/java/site/sonisori/sonisori/auth/jwt/JwtUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package site.sonisori.sonisori.auth.jwt; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Date; | ||
import java.util.UUID; | ||
|
||
import javax.crypto.SecretKey; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.Keys; | ||
import io.jsonwebtoken.security.SignatureException; | ||
import site.sonisori.sonisori.auth.jwt.dto.TokenDto; | ||
import site.sonisori.sonisori.auth.jwt.entity.RefreshToken; | ||
import site.sonisori.sonisori.auth.jwt.repository.RefreshTokenRepository; | ||
import site.sonisori.sonisori.common.constants.ErrorMessage; | ||
import site.sonisori.sonisori.entity.User; | ||
|
||
@Component | ||
public class JwtUtil { | ||
private static final int MILLIS = 1000; | ||
private final RefreshTokenRepository refreshTokenRepository; | ||
private final String issuer; | ||
private final long accessTokenExpiration; | ||
private final long refreshTokenExpiration; | ||
private final SecretKey secretKey; | ||
|
||
public JwtUtil(RefreshTokenRepository refreshTokenRepository, | ||
@Value("${spring.jwt.secret-key}") String secret, | ||
@Value("${spring.application.name}") String issuer, | ||
@Value("${spring.jwt.access-expiration}") long accessTokenExpiration, | ||
@Value("${spring.jwt.refresh-expiration}") long refreshTokenExpiration) { | ||
this.refreshTokenRepository = refreshTokenRepository; | ||
this.issuer = issuer; | ||
this.accessTokenExpiration = accessTokenExpiration * MILLIS; | ||
this.refreshTokenExpiration = refreshTokenExpiration * MILLIS; | ||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
public String createAccessToken(User user) { | ||
Date now = new Date(); | ||
Date expiryDate = new Date(now.getTime() + accessTokenExpiration); | ||
|
||
return Jwts.builder() | ||
.issuer(issuer) | ||
.issuedAt(now) | ||
.subject(String.valueOf(user.getId())) | ||
.claim("token_type", "access_token") | ||
.claim("role", user.getRole()) | ||
.expiration(expiryDate) | ||
.signWith(secretKey) | ||
.compact(); | ||
} | ||
|
||
public String createRefreshToken(User user) { | ||
RefreshToken refreshToken = new RefreshToken(UUID.randomUUID().toString(), user.getId()); | ||
refreshTokenRepository.save(refreshToken); | ||
return refreshToken.getRefreshToken(); | ||
} | ||
|
||
public TokenDto generateJwt(User user) { | ||
String accessToken = createAccessToken(user); | ||
String refreshToken = createRefreshToken(user); | ||
|
||
long currentMillis = System.currentTimeMillis(); | ||
|
||
return TokenDto.builder() | ||
.accessToken(accessToken) | ||
.accessTokenExpiresIn(currentMillis + accessTokenExpiration) | ||
.refreshToken(refreshToken) | ||
.refreshTokenExpiresIn(currentMillis + refreshTokenExpiration) | ||
.build(); | ||
} | ||
|
||
public Claims extractClaims(String accessToken) { | ||
try { | ||
return Jwts.parser().verifyWith(secretKey).build() | ||
.parseSignedClaims(accessToken).getPayload(); | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public boolean validateAccessToken(String accessToken) { | ||
try { | ||
Claims claims = extractClaims(accessToken); | ||
if (!claims.get("token_type").equals("access_token") || accessToken == null) { | ||
throw new JwtException(ErrorMessage.NOT_FOUND_TOKEN.getMessage()); | ||
} | ||
return !claims.getExpiration().before(new Date()); | ||
} catch (ExpiredJwtException e) { | ||
throw new JwtException(ErrorMessage.NOT_FOUND_TOKEN.getMessage()); | ||
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) { | ||
throw new JwtException(ErrorMessage.INVALID_TOKEN.getMessage()); | ||
} catch (JwtException e) { | ||
throw new JwtException(e.getMessage()); | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/site/sonisori/sonisori/auth/jwt/dto/TokenDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package site.sonisori.sonisori.auth.jwt.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record TokenDto( | ||
@NotBlank | ||
String accessToken, | ||
|
||
@NotBlank | ||
Long accessTokenExpiresIn, | ||
|
||
@NotBlank | ||
String refreshToken, | ||
|
||
@NotBlank | ||
Long refreshTokenExpiresIn | ||
) { | ||
} |
54 changes: 54 additions & 0 deletions
54
src/main/java/site/sonisori/sonisori/auth/jwt/exception/JwtExceptionFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package site.sonisori.sonisori.auth.jwt.exception; | ||
|
||
import java.io.IOException; | ||
|
||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.SignatureException; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import site.sonisori.sonisori.common.constants.ErrorMessage; | ||
import site.sonisori.sonisori.common.response.ErrorResponse; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtExceptionFilter extends OncePerRequestFilter { | ||
private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
throws ServletException, IOException { | ||
response.setCharacterEncoding("utf-8"); | ||
|
||
try { | ||
filterChain.doFilter(request, response); | ||
} catch (ExpiredJwtException e) { | ||
setErrorResponse(response, ErrorMessage.EXPIRED_TOKEN.getMessage()); | ||
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) { | ||
setErrorResponse(response, ErrorMessage.INVALID_TOKEN.getMessage()); | ||
} catch (JwtException e) { | ||
setErrorResponse(response, ErrorMessage.NOT_FOUND_TOKEN.getMessage()); | ||
} | ||
} | ||
|
||
private void setErrorResponse(HttpServletResponse response, String message) | ||
throws IOException { | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
response.setContentType("application/json"); | ||
|
||
ErrorResponse errorResponse = new ErrorResponse(message); | ||
|
||
String jsonResponse = objectMapper.writeValueAsString(errorResponse); | ||
response.getWriter().write(jsonResponse); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
src/main/java/site/sonisori/sonisori/auth/oauth2/CustomOAuth2User.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package site.sonisori.sonisori.auth.oauth2; | ||
|
||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.Map; | ||
|
||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import site.sonisori.sonisori.auth.oauth2.dto.OAuth2UserDto; | ||
import site.sonisori.sonisori.entity.User; | ||
|
||
@RequiredArgsConstructor | ||
public class CustomOAuth2User implements OAuth2User, UserDetails { | ||
private final OAuth2UserDto oAuth2UserDto; | ||
@Getter | ||
private final User user; | ||
private Map<String, Object> attributes; | ||
|
||
@Override | ||
public Map<String, Object> getAttributes() { | ||
return attributes; | ||
} | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
String role = oAuth2UserDto.role(); | ||
return Collections.singletonList(new SimpleGrantedAuthority(role)); | ||
} | ||
|
||
@Override | ||
public String getPassword() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return oAuth2UserDto.name(); | ||
} | ||
|
||
public String getUsername() { | ||
return oAuth2UserDto.username(); | ||
} | ||
|
||
public Long getUserId() { | ||
return user.getId(); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/main/java/site/sonisori/sonisori/auth/oauth2/dto/OAuth2UserDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package site.sonisori.sonisori.auth.oauth2.dto; | ||
|
||
public record OAuth2UserDto( | ||
String name, | ||
String username, | ||
String role, | ||
String email | ||
) { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
src/main/java/site/sonisori/sonisori/config/ExceptionHandlerConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package site.sonisori.sonisori.config; | ||
|
||
import java.io.IOException; | ||
|
||
import org.springframework.security.access.AccessDeniedException; | ||
import org.springframework.security.config.Customizer; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.stereotype.Component; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import site.sonisori.sonisori.common.constants.ErrorMessage; | ||
import site.sonisori.sonisori.common.response.ErrorResponse; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class ExceptionHandlerConfig implements Customizer<ExceptionHandlingConfigurer<HttpSecurity>> { | ||
private final ObjectMapper objectMapper; | ||
|
||
@Override | ||
public void customize(ExceptionHandlingConfigurer<HttpSecurity> httpSecurityExceptionHandlingConfigurer) { | ||
httpSecurityExceptionHandlingConfigurer | ||
.authenticationEntryPoint(this::handleAuthenticationException) | ||
.accessDeniedHandler(this::handleAccessDeniedException); | ||
} | ||
|
||
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, | ||
AuthenticationException authException) throws IOException { | ||
log.error("Unauthorized request - Method: {}, URI: {}, Error: {}", | ||
request.getMethod(), | ||
request.getRequestURI(), | ||
authException.getMessage()); | ||
|
||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
response.setContentType("application/json"); | ||
|
||
ErrorResponse errorResponse = new ErrorResponse(ErrorMessage.NOT_FOUND_TOKEN.getMessage()); | ||
|
||
String jsonResponse = objectMapper.writeValueAsString(errorResponse); | ||
response.getWriter().write(jsonResponse); | ||
} | ||
|
||
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, | ||
AccessDeniedException accessDeniedException) throws IOException { | ||
response.sendError(HttpServletResponse.SC_FORBIDDEN); | ||
} | ||
} |
Oops, something went wrong.