Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.sillim.recordit.config.security.encrypt;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AESEncryptor {

public static final String AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding";

public String encrypt(String strToEncrypt, String secret) throws Exception {
SecretKeySpec secretKey = createKey(secret);
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS5PADDING);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)));
}

public String decrypt(String strToDecrypt, String secret) throws Exception {
SecretKeySpec secretKey = createKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
}

public SecretKeySpec createKey(String secret) throws NoSuchAlgorithmException {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] key = sha.digest(Base64.getDecoder().decode(secret.getBytes(StandardCharsets.UTF_8)));
key = Arrays.copyOf(key, 16);
return new SecretKeySpec(key, "AES");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.sillim.recordit.config.security.encrypt;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CryptoAlgorithms {
SHA_256("SHA-256"), AES("AES"), AES_ECB_PKCS5PADDING("AES/ECB/PKCS5Padding"), AES_CBC_PKCS5PADDING(
"AES/CBC/PKCS5Padding"),;

private String value;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.sillim.recordit.config.security.filter;

import com.sillim.recordit.config.security.jwt.JwtValidator;
import com.sillim.recordit.global.exception.ErrorCode;
import com.sillim.recordit.global.exception.common.ApplicationException;
import com.sillim.recordit.member.domain.AuthorizedUser;
import com.sillim.recordit.member.mapper.AuthorizedUserMapper;
import com.sillim.recordit.member.service.MemberQueryService;
Expand Down Expand Up @@ -30,22 +32,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
getTokenFromHeader(request).ifPresent(this::authenticate);
getTokenFromHeader(request).ifPresent(token -> {
try {
authenticate(token);
} catch (Exception e) {
throw new ApplicationException(ErrorCode.UNHANDLED_EXCEPTION, e.getMessage());
}
});

doFilter(request, response, filterChain);
}

private void authenticate(String token) {
private void authenticate(String token) throws Exception {
if (isBearerType(token)) {
AuthorizedUser authorizedUser = getAuthorizedUser(token);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(authorizedUser, "", authorizedUser.getAuthorities()));
}
}

private AuthorizedUser getAuthorizedUser(String token) {
private AuthorizedUser getAuthorizedUser(String token) throws Exception {
return authorizedUserMapper.toAuthorizedUser(
memberQueryService.findByMemberId(jwtValidator.getMemberIdIfValid(token.substring(BEARER.length()))));
memberQueryService.findByEmail(jwtValidator.getSubIfValid(token.substring(BEARER.length()))));
}

private Optional<String> getTokenFromHeader(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.sillim.recordit.config.security.jwt;

import com.sillim.recordit.config.security.encrypt.AESEncryptor;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
Expand All @@ -8,41 +9,47 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtProvider {

private final Key secretKey;
@Value("${jwt.secret-key}")
private String secret;

private static final Long EXCHANGE_TOKEN_VALIDATION_SECOND = 60L * 1000;
private static final Long ACCESS_TOKEN_VALIDATION_SECOND = 60L * 60 * 24 * 1000;
private static final Long REFRESH_TOKEN_VALIDATION_SECOND = 60L * 60 * 24 * 14 * 1000;

public String generateExchangeToken(Long memberId) {
return buildToken(memberId)
.setExpiration(Date.from(Instant.now().plus(EXCHANGE_TOKEN_VALIDATION_SECOND, ChronoUnit.SECONDS)))
private final Key secretKey;
private final AESEncryptor encryptor;

public String generateExchangeToken(String email) throws Exception {
return buildToken(email)
.setExpiration(Date.from(Instant.now().plus(EXCHANGE_TOKEN_VALIDATION_SECOND, ChronoUnit.MILLIS)))
.compact();
}

public AuthorizationToken generateAuthorizationToken(Long memberId) {
return new AuthorizationToken(generateAccessToken(memberId), generateRefreshToken(memberId));
public AuthorizationToken generateAuthorizationToken(String email) throws Exception {
return new AuthorizationToken(generateAccessToken(email), generateRefreshToken(email));
}

private String generateAccessToken(Long memberId) {
return buildToken(memberId)
.setExpiration(Date.from(Instant.now().plus(ACCESS_TOKEN_VALIDATION_SECOND, ChronoUnit.SECONDS)))
private String generateAccessToken(String email) throws Exception {
return buildToken(email)
.setExpiration(Date.from(Instant.now().plus(ACCESS_TOKEN_VALIDATION_SECOND, ChronoUnit.MILLIS)))
.compact();
}

private String generateRefreshToken(Long memberId) {
return buildToken(memberId)
.setExpiration(Date.from(Instant.now().plus(REFRESH_TOKEN_VALIDATION_SECOND, ChronoUnit.SECONDS)))
private String generateRefreshToken(String email) throws Exception {
return buildToken(email)
.setExpiration(Date.from(Instant.now().plus(REFRESH_TOKEN_VALIDATION_SECOND, ChronoUnit.MILLIS)))
.compact();
}

private JwtBuilder buildToken(Long memberId) {
return Jwts.builder().setSubject(String.valueOf(memberId)).signWith(secretKey, SignatureAlgorithm.HS512);
private JwtBuilder buildToken(String email) throws Exception {
return Jwts.builder().setSubject(encryptor.encrypt(email, secret)).signWith(secretKey,
SignatureAlgorithm.HS512);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.sillim.recordit.config.security.jwt;

import com.sillim.recordit.config.security.encrypt.AESEncryptor;
import com.sillim.recordit.global.exception.ErrorCode;
import com.sillim.recordit.global.exception.security.InvalidJwtException;
import io.jsonwebtoken.Claims;
Expand All @@ -12,21 +13,26 @@
import java.security.Key;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtValidator {

@Value("${jwt.secret-key}")
private String secret;

private final Key secretKey;
private final AESEncryptor encryptor;

public Long getMemberIdIfValid(String accessToken) {
String memberId = validateToken(accessToken).getBody().getSubject();
public String getSubIfValid(String accessToken) throws Exception {
String sub = validateToken(accessToken).getBody().getSubject();

if (Objects.isNull(memberId)) {
throw new InvalidJwtException(ErrorCode.JWT_UNSUPPORTED, "유저 ID를 찾을 수 없습니다.");
if (Objects.isNull(sub)) {
throw new InvalidJwtException(ErrorCode.JWT_UNSUPPORTED, "Subject를 찾을 수 없습니다.");
}
return Long.valueOf(memberId);
return encryptor.decrypt(sub, secret);
}

public Jws<Claims> validateToken(String token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ public Collection<? extends GrantedAuthority> getAuthorities() {

@Override
public String getName() {
return member.getId().toString();
}

public Long getId() {
return member.getId();
return member.getEmail();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.sillim.recordit.config.security.jwt.JwtProvider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
Expand All @@ -24,9 +23,13 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
Authentication authentication) {
LoginMember loginMember = (LoginMember) authentication.getPrincipal();
getRedirectStrategy().sendRedirect(request, response,
clientUrl + redirectEndpoint + "?token=" + jwtProvider.generateExchangeToken(loginMember.getId()));
try {
getRedirectStrategy().sendRedirect(request, response, clientUrl + redirectEndpoint + "?token="
+ jwtProvider.generateExchangeToken(loginMember.getName()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
Expand All @@ -29,12 +28,12 @@ public class LoginController {
private final LoginService loginService;

@PostMapping("/login")
public ResponseEntity<OAuthTokenResponse> login(@RequestBody LoginRequest loginRequest) throws IOException {
public ResponseEntity<OAuthTokenResponse> login(@RequestBody LoginRequest loginRequest) throws Exception {
return ResponseEntity.ok(loginService.login(loginRequest));
}

@PostMapping("/web-login")
public ResponseEntity<OAuthTokenResponse> webLogin(@RequestBody WebLoginRequest webLoginRequest) {
public ResponseEntity<OAuthTokenResponse> webLogin(@RequestBody WebLoginRequest webLoginRequest) throws Exception {
return ResponseEntity.ok(loginService.login(webLoginRequest.exchangeToken()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public interface MemberRepository extends Neo4jRepository<Member, Long> {
@Query("MATCH (m:Member) WHERE m.oauthAccount = $oauthAccount RETURN m")
Optional<Member> findByOauthAccount(@Param("oauthAccount") String oauthAccount);

Optional<Member> findByEmail(String email);

List<Member> findByPersonalIdStartingWith(String prefix);

@Query("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ public LoginService(JwtProvider jwtProvider, JwtValidator jwtValidator, ObjectMa
authenticationServiceMap.put(OAuthProvider.NAVER, naverAuthenticationService);
}

public OAuthTokenResponse login(String exchangeToken) {
Long memberId = jwtValidator.getMemberIdIfValid(exchangeToken);
AuthorizationToken token = jwtProvider.generateAuthorizationToken(memberId);
Optional<Member> member = memberRepository.findById(memberId);
public OAuthTokenResponse login(String exchangeToken) throws Exception {
String sub = jwtValidator.getSubIfValid(exchangeToken);
AuthorizationToken token = jwtProvider.generateAuthorizationToken(sub);
Optional<Member> member = memberRepository.findByEmail(sub);
boolean activated = false;
if (member.isPresent()) {
activated = member.get().getActivated();
Expand All @@ -70,7 +70,7 @@ public OAuthTokenResponse login(String exchangeToken) {
return new OAuthTokenResponse(token.accessToken(), token.refreshToken(), activated);
}

public OAuthTokenResponse login(LoginRequest loginRequest) throws IOException {
public OAuthTokenResponse login(LoginRequest loginRequest) throws Exception {
AuthenticationService authenticationService = authenticationServiceMap.get(loginRequest.provider());

if (loginRequest.provider().equals(OAuthProvider.NAVER)) {
Expand All @@ -84,7 +84,7 @@ public OAuthTokenResponse login(LoginRequest loginRequest) throws IOException {
memberDeviceService.addMemberDeviceIfNotExists(loginRequest.deviceId(), loginRequest.model(),
loginRequest.fcmToken(), member);

AuthorizationToken token = jwtProvider.generateAuthorizationToken(member.getId());
AuthorizationToken token = jwtProvider.generateAuthorizationToken(member.getEmail());
return new OAuthTokenResponse(token.accessToken(), token.refreshToken(), member.getActivated());
}

Expand All @@ -96,14 +96,14 @@ public void activateMember(String personalId, Long memberId) {
memberRepository.save(member);
}

private OAuthTokenResponse loginWithoutOidc(LoginRequest loginRequest,
AuthenticationService authenticationService) {
private OAuthTokenResponse loginWithoutOidc(LoginRequest loginRequest, AuthenticationService authenticationService)
throws Exception {
MemberInfo memberInfo = authenticationService.getMemberInfoByAccessToken(loginRequest.accessToken());
Member member = memberRepository.findByOauthAccount(memberInfo.oauthAccount())
.orElseGet(() -> signupService.signup(memberInfo));
member = validateQuickRejoinMember(member, memberInfo);

AuthorizationToken token = jwtProvider.generateAuthorizationToken(member.getId());
AuthorizationToken token = jwtProvider.generateAuthorizationToken(member.getEmail());
return new OAuthTokenResponse(token.accessToken(), token.refreshToken(), member.getActivated());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public Member findByMemberId(Long memberId) {
.orElseThrow(() -> new RecordNotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}

public Member findByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new RecordNotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}

public ProfileResponse searchProfileByMemberId(Long memberId, Long myId) {
Member me = findByMemberId(myId);
Member other = findByMemberId(memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ void initSecurityContext() {

@Test
@DisplayName("헤더에 유효한 Bearer Token이 있으면 인증에 성공한다.")
void authenticatedIfExistsTokenInHeader() throws ServletException, IOException {
void authenticatedIfExistsTokenInHeader() throws Exception {
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
FilterChain mockFilterChain = mock(FilterChain.class);
String token = "Bearer token";
httpServletRequest.addHeader(HttpHeaders.AUTHORIZATION, token);
long memberId = 1L;
String email = "[email protected]";
Member member = Member.createNoJobMember("12345", OAuthProvider.KAKAO, "name", "[email protected]",
"https://image.url");
given(jwtValidator.getMemberIdIfValid(eq("token"))).willReturn(memberId);
given(memberQueryService.findByMemberId(eq(memberId))).willReturn(member);
given(jwtValidator.getSubIfValid(eq("token"))).willReturn(email);
given(memberQueryService.findByEmail(eq(email))).willReturn(member);
given(authorizedUserMapper.toAuthorizedUser(member)).willReturn(new AuthorizedUser(member, null, null));

jwtAuthenticationFilter.doFilterInternal(httpServletRequest, httpServletResponse, mockFilterChain);
Expand Down Expand Up @@ -93,13 +93,13 @@ void notAuthenticatedIfNotExistsTokenInHeader() throws ServletException, IOExcep

@Test
@DisplayName("헤더에 Token이 유효하지 않으면 인증에 실패한다.")
void notAuthenticatedIfTokenIsInvalid() throws ServletException, IOException {
void notAuthenticatedIfTokenIsInvalid() throws Exception {
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
FilterChain mockFilterChain = mock(FilterChain.class);
String token = "Bearer token";
httpServletRequest.addHeader(HttpHeaders.AUTHORIZATION, token);
given(jwtValidator.getMemberIdIfValid(eq("token"))).willThrow(MalformedJwtException.class);
given(jwtValidator.getSubIfValid(eq("token"))).willThrow(MalformedJwtException.class);

assertThatThrownBy(() -> jwtAuthenticationFilter.doFilterInternal(httpServletRequest, httpServletResponse,
mockFilterChain)).isInstanceOf(MalformedJwtException.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@ class JwtProviderTest {

@Test
@DisplayName("멤버 ID를 통해 인가 토큰을 생성한다.")
void generateAuthorizationTokenByMemberId() {
long memberId = 1L;
void generateAuthorizationTokenByMemberId() throws Exception {
String signature = "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest"
+ "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest" + "testtest";
String email = "[email protected]";
Key secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(signature.getBytes()));
ReflectionTestUtils.setField(jwtProvider, "secretKey", secretKey);

AuthorizationToken token = jwtProvider.generateAuthorizationToken(memberId);
AuthorizationToken token = jwtProvider.generateAuthorizationToken(email);

assertThat(Long.parseLong(Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token.accessToken()).getBody().getSubject())).isEqualTo(memberId);
.parseClaimsJws(token.accessToken()).getBody().getSubject())).isEqualTo(email);
assertThat(Long.parseLong(Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token.refreshToken()).getBody().getSubject())).isEqualTo(memberId);
.parseClaimsJws(token.refreshToken()).getBody().getSubject())).isEqualTo(email);
}
}
Loading
Loading