Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[INIT] 시큐리티 및 jwt 기초 세팅 #42

Merged
merged 19 commits into from
Jan 10, 2024
Merged
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
10 changes: 9 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ dependencies {
// https://mvnrepository.com/artifact/org.postgresql/postgresql
implementation 'org.postgresql:postgresql:42.7.1'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// gson
implementation 'com.google.code.gson:gson:2.8.6'

// restdocs-swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
Expand Down Expand Up @@ -85,4 +93,4 @@ openapi3 {
outputFileNamePrefix = 'open-api-3.0.1'
format = 'json'
outputDirectory = 'build/resources/main/static/docs'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.soptie.server.auth.controller;

import com.soptie.server.auth.dto.SignInRequest;
import com.soptie.server.auth.message.ResponseMessage;
import com.soptie.server.auth.service.AuthService;
import com.soptie.server.common.dto.Response;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static com.soptie.server.common.dto.Response.success;

@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/auth")
public class AuthController {

private final AuthService authService;
thguss marked this conversation as resolved.
Show resolved Hide resolved

@PostMapping
public ResponseEntity<Response> signIn(@RequestHeader("Authorization") String socialAccessToken, @RequestBody SignInRequest request) {
val response = authService.signIn(socialAccessToken, request);
return ResponseEntity.ok(success(ResponseMessage.SUCCESS_SIGNIN.getMessage(), response));
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/soptie/server/auth/dto/SignInRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.soptie.server.auth.dto;

import com.soptie.server.member.entity.SocialType;

public record SignInRequest(
SocialType socialType
) {

public static SignInRequest of(SocialType socialType) {
return new SignInRequest(socialType);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/soptie/server/auth/dto/SignInResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.soptie.server.auth.dto;

import com.soptie.server.auth.vo.Token;
import lombok.Builder;

@Builder
public record SignInResponse(
String accessToken,
String refreshToken
) {

public static SignInResponse of(Token token) {
return SignInResponse.builder()
.accessToken(token.getAccessToken())
.refreshToken(token.getRefreshToken())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.soptie.server.auth.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.soptie.server.common.dto.Response;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static com.soptie.server.auth.message.ErrorMessage.INVALID_TOKEN;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Component
@RequiredArgsConstructor
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
setResponse(response);
}

private void setResponse(HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(SC_UNAUTHORIZED);
response.getWriter().println(objectMapper.writeValueAsString(Response.fail(INVALID_TOKEN.getMessage())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.soptie.server.auth.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

import static com.soptie.server.auth.jwt.JwtValidationType.VALID_JWT;
import static io.jsonwebtoken.lang.Strings.hasText;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String BEARER_HEADER = "Bearer ";
private static final String BLANK = "";

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
val token = getAccessTokenFromRequest(request);
if (hasText(token) && jwtTokenProvider.validateToken(token) == VALID_JWT) {
val authentication = new UserAuthentication(getMemberId(token), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
log.error(exception.getMessage());
}

filterChain.doFilter(request, response);
}

private Long getMemberId(String token) {
return jwtTokenProvider.getUserFromJwt(token);
}

private String getAccessTokenFromRequest(HttpServletRequest request) {
return isContainsAccessToken(request) ? getAuthorizationAccessToken(request) : null;
}

private boolean isContainsAccessToken(HttpServletRequest request) {
val authorization = request.getHeader(AUTHORIZATION);
return authorization != null && authorization.startsWith(BEARER_HEADER);
}

private String getAuthorizationAccessToken(HttpServletRequest request) {
return request.getHeader(AUTHORIZATION).replaceFirst(BEARER_HEADER, BLANK);
}
}
78 changes: 78 additions & 0 deletions src/main/java/com/soptie/server/auth/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.soptie.server.auth.jwt;

import com.soptie.server.common.config.ValueConfig;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

import static com.soptie.server.auth.jwt.JwtValidationType.*;
import static io.jsonwebtoken.Header.*;
import static io.jsonwebtoken.security.Keys.hmacShaKeyFor;
import static java.util.Base64.getEncoder;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private final ValueConfig valueConfig;

public String generateToken(Authentication authentication, Long expiration) {
return Jwts.builder()
.setHeaderParam(TYPE, JWT_TYPE)
.setClaims(generateClaims(authentication))
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}

public JwtValidationType validateToken(String token) {
try {
getBody(token);
return VALID_JWT;
} catch (MalformedJwtException exception) {
log.error(exception.getMessage());
return INVALID_JWT_TOKEN;
} catch (ExpiredJwtException exception) {
log.error(exception.getMessage());
return EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException exception) {
log.error(exception.getMessage());
return UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException exception) {
log.error(exception.getMessage());
return EMPTY_JWT;
}
}

private Claims generateClaims(Authentication authentication) {
val claims = Jwts.claims();
claims.put("memberId", authentication.getPrincipal());
return claims;
}

public Long getUserFromJwt(String token) {
val claims = getBody(token);
return Long.parseLong(claims.get("memberId").toString());
}

private Claims getBody(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private SecretKey getSigningKey() {
val encodedKey = getEncoder().encodeToString(valueConfig.getSecretKey().getBytes());
return hmacShaKeyFor(encodedKey.getBytes());
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/soptie/server/auth/jwt/JwtValidationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.soptie.server.auth.jwt;

public enum JwtValidationType {
VALID_JWT,
INVALID_JWT_SIGNATURE,
INVALID_JWT_TOKEN,
EXPIRED_JWT_TOKEN,
UNSUPPORTED_JWT_TOKEN,
EMPTY_JWT
}
13 changes: 13 additions & 0 deletions src/main/java/com/soptie/server/auth/jwt/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.soptie.server.auth.jwt;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {

public UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/soptie/server/auth/message/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.soptie.server.auth.message;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum ErrorMessage {

EMPTY_ACCESS_TOKEN("액세스 토큰이 없습니다."),
EMPTY_REFRESH_TOKEN("리프레시 토큰이 없습니다"),
INVALID_TOKEN("유효하지 않은 토큰입니다"),
MESSAGE_UNAUTHORIZED("유효하지 않은 토큰"),
;

private final String message;
}
13 changes: 13 additions & 0 deletions src/main/java/com/soptie/server/auth/message/ResponseMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.soptie.server.auth.message;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum ResponseMessage {

SUCCESS_SIGNIN("소셜로그인 성공");

private final String message;
}
9 changes: 9 additions & 0 deletions src/main/java/com/soptie/server/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.soptie.server.auth.service;

import com.soptie.server.auth.dto.SignInRequest;
import com.soptie.server.auth.dto.SignInResponse;

public interface AuthService {

SignInResponse signIn(String socialAccessToken, SignInRequest request);
}
Loading