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 1 commit
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
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ 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'

implementation 'com.google.code.gson:gson:2.8.6'
thguss marked this conversation as resolved.
Show resolved Hide resolved

// 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 +92,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,32 @@
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/oauth")
thguss marked this conversation as resolved.
Show resolved Hide resolved
public class AuthController {

private final AuthService authService;
thguss marked this conversation as resolved.
Show resolved Hide resolved
/*
@ResponseBody
@GetMapping("/kakao")
public void kakaoCallback(@RequestParam String code) throws Exception {
System.out.println(authService.getKakaoAccessToken(code));
}*/
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) {
thguss marked this conversation as resolved.
Show resolved Hide resolved
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,33 @@
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 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
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper = new ObjectMapper();
thguss marked this conversation as resolved.
Show resolved Hide resolved

@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,51 @@
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 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 int BEARER_HEADER_LENGTH = 7;
thguss marked this conversation as resolved.
Show resolved Hide resolved

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String token = getAccessTokenFromRequest(request);
if (hasText(token) && jwtTokenProvider.validateToken(token) == VALID_JWT) {
Long userId = jwtTokenProvider.getUserFromJwt(token);
thguss marked this conversation as resolved.
Show resolved Hide resolved
UserAuthentication authentication = new UserAuthentication(userId, null, null); // 아이디, 비밀번호, 권한
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
log.error("error : ", exception);
}

filterChain.doFilter(request, response);
}

private String getAccessTokenFromRequest(HttpServletRequest request) {
String authorization = request.getHeader(AUTHORIZATION);
return (hasText(authorization) && authorization.startsWith(BEARER_HEADER)) ? authorization.substring(BEARER_HEADER_LENGTH) : null;
thguss marked this conversation as resolved.
Show resolved Hide resolved
thguss marked this conversation as resolved.
Show resolved Hide resolved
}
}
79 changes: 79 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,79 @@
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 org.springframework.beans.factory.annotation.Value;
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 ex) {
log.error(String.valueOf(INVALID_JWT_TOKEN));
thguss marked this conversation as resolved.
Show resolved Hide resolved
System.out.println(ex.getMessage());
return INVALID_JWT_TOKEN;
} catch (ExpiredJwtException ex) {
log.error(String.valueOf(EXPIRED_JWT_TOKEN));
return EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException ex) {
log.error(String.valueOf(UNSUPPORTED_JWT_TOKEN));
return UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException ex) {
log.error(String.valueOf(EMPTY_JWT));
return EMPTY_JWT;
}
}

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

public Long getUserFromJwt(String token) {
Claims 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() {
String 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);
}
}
23 changes: 23 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,23 @@
package com.soptie.server.auth.message;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum ErrorMessage {

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

// member
EMPTY_MEMBER("존재하지 않는 회원입니다."),
DUPLICATE_USERNAME("이미 존재하는 닉네임입니다."),
INVALID_MEMBER("유효하지 않은 회원입니다."),
INVALID_USERNAME("유효하지 않은 닉네임입니다.");
thguss marked this conversation as resolved.
Show resolved Hide resolved

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;
}
Loading