Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.sillim.recordit.config.security.encrypt;

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

@Component
@RequiredArgsConstructor
public class AESEncryptor {

private static final String AES = "AES";
private static final String SHA_256 = "SHA-256";
private static final String AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding";

public String encrypt(String strToEncrypt, String secret) throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
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 NoSuchAlgorithmException, NoSuchPaddingException,
IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
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 All @@ -9,7 +11,12 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -30,22 +37,31 @@ 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 (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException
| InvalidKeyException e) {
throw new ApplicationException(ErrorCode.UNHANDLED_EXCEPTION, e.getMessage());
}
});

doFilter(request, response, filterChain);
}

private void authenticate(String token) {
private void authenticate(String token) throws NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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,48 +1,64 @@
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;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.sql.Date;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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 @@ -9,24 +10,35 @@
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
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 NoSuchPaddingException, IllegalBlockSizeException,
NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
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
Loading
Loading