Skip to content

Commit

Permalink
Merge pull request #42 from Team-Sopetit/feature/#4-social-login-api
Browse files Browse the repository at this point in the history
[INIT] 시큐리티 및 jwt 기초 세팅 및 카카오 로그인 구현
  • Loading branch information
Chan531 authored Jan 10, 2024
2 parents 554d3e5 + 6fa0f0f commit d714ee4
Show file tree
Hide file tree
Showing 24 changed files with 612 additions and 2 deletions.
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;

@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

0 comments on commit d714ee4

Please sign in to comment.