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

✨ [FEAT] 인터셉터로 백준 쿠키 redis 상주 확인 및 익스텐션에 예외처리 #452 #458

Merged
merged 4 commits into from
Oct 17, 2023
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
4 changes: 0 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,5 @@ FROM openjdk:17-oracle
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

COPY temp.cpp /codes/temp.cpp
COPY temp.py /codes/temp.py
COPY Main.java /codes/Main.java

ENTRYPOINT ["java","-jar","/app.jar"]

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public CorsConfigurationSource corsConfigurationSource() {
"https://morandi.co.kr",
"http://api.morandi.co.kr",
"https://api.morandi.co.kr",
"chrome-extension://*",
"chrome-extension://ljkmahbkojffhjdjkghaljooajocajnf",//배포된 크롬 익스텐션
"chrome-extension://cmblaiddbfchipealeopkbbnboifeedc",
"chrome-extension://ckepgfjakcdkjpabldbamcfcjhcdojih"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
Expand All @@ -33,4 +33,4 @@ public CorsConfigurationSource corsConfigurationSource() {
return source;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final AuthUserDetailService authUserDetailService;

private final ObjectMapper mapper = new ObjectMapper();

private String getJwtFromRequest(HttpServletRequest request) {
Expand All @@ -43,43 +44,54 @@ private String getJwtFromRequest(HttpServletRequest request) {
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;


//토큰이 없으면 예외처리
throw new MorandiException(AuthErrorCode.TOKEN_NOT_FOUND, "토큰이 없습니다.");

}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


//가입 관련 요청 통과하게
if(request.getRequestURI().startsWith("/oauths")){
filterChain.doFilter(request,response);
return;
}

String token = getJwtFromRequest(request);

if(StringUtils.hasText(token) && jwtProvider.validateToken(token))
if(StringUtils.hasText(token)&&jwtProvider.validateToken(token))
{

Long memberId = jwtProvider.getUserIdfromToken(token);

AuthDetails userDetails = authUserDetailService.loadUserByUsername(memberId.toString());

Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authentication);

//쿠키 등록 요청은 통과하게
if (request.getRequestURI().startsWith("/submit/cookie")) {
filterChain.doFilter(request,response);
return;
}
//스웨거
if(request.getRequestURI().startsWith("/swagger-ui/") || request.getRequestURI().startsWith("/v3/api-docs") || request.getRequestURI().startsWith("/swagger-resources") ){
filterChain.doFilter(request,response);
return;
}
//초기 정보인 백준 ID가 아직 등록되지 않았으면 예외처리 (초기값 설정 유도)
if (!(request.getRequestURI().equals("/members/register-info") && request.getMethod().equals("POST"))
&& !(request.getRequestURI().startsWith("/swagger-ui/") || request.getRequestURI().startsWith("/v3/api-docs"))
&&!(request.getRequestURI().startsWith("/oauths"))
&& userDetails.getBojId() == null) {
if (!(request.getRequestURI().equals("/members/register-info")) && userDetails.getBojId() == null) {
String msg = String.format("[clientIP]: %s, [clientURL]: %s,",
request.getRemoteAddr(),
request.getRequestURL().toString()
);
log.error("[REQUEST] memberId={} 의 백준 ID가 등록되지 않았습니다. {}, [parameter]: {}",memberId, msg, mapper.writeValueAsString(request.getParameterMap()));

log.error("[REQUEST] memberId={} 의 백준 ID가 등록되지 않았습니다. 크롬 익스텐션을 설치해주세요. {}, [parameter]: {}",memberId, msg, mapper.writeValueAsString(request.getParameterMap()));
throw new MorandiException(AuthErrorCode.BAEKJOON_ID_NULL);
}
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authentication);
}
// else
// {
// throw new MorandiException(AuthErrorCode.INVALID_TOKEN);
// }

filterChain.doFilter(request,response);


Expand Down
2 changes: 2 additions & 0 deletions src/main/java/swm_nm/morandi/config/security/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
public class JwtProvider {
private final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
private Optional<Jws<Claims>> parseTokenToJws(String token){
if(token==null)
throw new MorandiException(AuthErrorCode.INVALID_TOKEN);
try {
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(getSecretKey())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,16 @@
public class SecurityConfig {

private final FilterConfig filterConfig;
//private final JwtAuthException jwtAuthException;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{

http
http
.httpBasic().disable()
.csrf().disable()
.cors()
.and()

.authorizeRequests()
.antMatchers("/oauths/**").permitAll()
.antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
.and()

.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package swm_nm.morandi.domain.codeSubmit.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -26,6 +27,7 @@
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@RequiredArgsConstructor
public class BaekjoonSubmitService {

Expand All @@ -46,8 +48,20 @@ private String generateKey(Long memberId) {
public String saveBaekjoonInfo(BaekjoonUserDto baekjoonUserDto) {
Long memberId = SecurityUtils.getCurrentMemberId();
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MorandiException(MemberErrorCode.MEMBER_NOT_FOUND));

.orElseThrow(() -> new MorandiException(MemberErrorCode.EXTENSION_MEMBER_NOT_FOUND));

//이미 백준 아이디가 존재하는 경우 -> TODO 크롬익스텐션에서 예외 잡아서 loginCookieValue null로 처리하고 팝업 하나 띄우기
if(member.getBojId()==null)
{
if (memberRepository.existsByBojId(baekjoonUserDto.getBojId())) {
throw new MorandiException(MemberErrorCode.DUPLICATED_BOJ_ID);
}
}
//백준 아이디가 기존에 저장된 id랑 다른 경우 -> TODO 크롬익스텐션에서 예외 잡아서 팝업 띄우고 기존 저장된 백준 id가 다르다고 알려주기
else if(!member.getBojId().equals(baekjoonUserDto.bojId))
{
throw new MorandiException(SubmitErrorCode.BAEKJOON_INVALID_ID);
}
String key = generateKey(memberId);
//Redis에 쿠키 저장
redisTemplate.opsForValue().set(key, baekjoonUserDto.getCookie());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {

// OAuth 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메소드
Optional<Member> findByEmail(String email);

Boolean existsByBojId(String bojId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public String generateKey(Tests test, int problemNumber, String language) {
@PostConstruct
public void initTempCodeInitializer() {
languages = List.of("Python", "Cpp", "Java");
String pythonCode = readCodeFromFile("codes/temp.py");
String cppCode = readCodeFromFile("codes/temp.cpp");
String javaCode = readCodeFromFile("codes/Main.java");
String pythonCode = readCodeFromFile("src/main/resources/codes/temp.py");
String cppCode = readCodeFromFile("src/main/resources/codes/temp.cpp");
String javaCode = readCodeFromFile("src/main/resources/codes/temp.java");
codeLists = List.of(pythonCode, cppCode, javaCode);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ public enum AuthErrorCode implements ErrorCode {
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED,"유효하지 않은 인증 정보입니다."),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"인증 시간이 만료된 토큰입니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED,"유효하지 않은 토큰입니다."),
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰을 찾을 수 없습니다"),
UNKNOWN_ERROR(HttpStatus.FORBIDDEN,"알 수 없는 오류" ),
SSO_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED,"SSO의 Access Token을 가져올 수 없습니다." ),
SSO_USERINFO(HttpStatus.UNAUTHORIZED,"SSO에서 사용자 정보를 가져올 수 없습니다" ),
SSO_SERVER_ERROR(HttpStatus.UNAUTHORIZED,"SSO서버와 통신할 수 없습니다." ),
BAEKJOON_ID_NULL(HttpStatus.FORBIDDEN,"백준 ID는 필수로 등록해야합니다. " ),
BAEKJOON_ID_NULL(HttpStatus.FORBIDDEN,"백준 ID는 크롬 익스텐션을 통해 필수로 등록해야합니다. " ),
INVALID_SOCIAL_TYPE(HttpStatus.UNAUTHORIZED,"지원되지 않는 OAuth provider 입니다.");


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
public enum MemberErrorCode implements ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.UNAUTHORIZED,"토큰에 해당하는 사용자를 찾을 수 없습니다"),
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"알 수 없는 오류" ),
BAEKJOONID_NOT_FOUND(HttpStatus.UNAUTHORIZED,"백준 아이디를 찾을 수 없습니다");
BAEKJOONID_NOT_FOUND(HttpStatus.UNAUTHORIZED,"백준 아이디를 찾을 수 없습니다"),
EXTENSION_MEMBER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR,"사용자가 아직 가입하지 않았습니다."),
DUPLICATED_BOJ_ID(HttpStatus.CONFLICT,"이미 등록된 백준 아이디입니다.");


private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ public enum SubmitErrorCode implements ErrorCode {
BAEKJOON_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"백준 서버 요청 오류"),
LANGUAGE_CODE_NOT_FOUND(HttpStatus.FORBIDDEN,"지원하는 언어 ID를 찾을 수 없습니다."),
BAEKJOON_LOGIN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"백준 로그인에 실패했습니다."),
COOKIE_NOT_EXIST(HttpStatus.INTERNAL_SERVER_ERROR,"쿠키가 존재하지 않습니다."),;
COOKIE_NOT_EXIST(HttpStatus.BAD_REQUEST,"쿠키가 존재하지 않습니다. 익스텐션을 실행해 주세요"),
BAEKJOON_INVALID_ID(HttpStatus.FORBIDDEN,"초기에 등록한 백준 ID로 로그인 해주세요");

//TODO
//크롬익스텐션에서 fetch요청 시 forbidden오면 저장된 쿠키 null로 변경하고, 다시 로그인하라는 메시지 띄우기
//익스텐션 레벨에서 마지막으로 확인한 시간을 알아내서 너무 오래됐으면 다시 쿠키 보내기

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public ResponseEntity<?> MorandiExceptionHandler(MorandiException e){
//log.error(message, e);
Sentry.configureScope(Scope::clear);


//인증 에러 발생 시 로그인 페이지로 이동 시키기, 인증 에러가 아닐 경우에는 에러 메시지만 반환하기
if (e.getErrorCode().getHttpStatus() ==HttpStatus.UNAUTHORIZED) {
HttpHeaders headers = new HttpHeaders();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package swm_nm.morandi.interceptor;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import swm_nm.morandi.global.exception.MorandiException;
import swm_nm.morandi.global.exception.errorcode.SubmitErrorCode;
import swm_nm.morandi.global.utils.SecurityUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Component
@RequiredArgsConstructor
public class BaekjoonLoginInterceptor implements HandlerInterceptor {


private final RedisTemplate<String, Object> redisTemplate;
private String generateKey(Long memberId) {
return String.format("OnlineJudgeCookie:memberId:%s", memberId);
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Long memberId = SecurityUtils.getCurrentMemberId();
String key = generateKey(memberId);
Boolean hasKey = redisTemplate.hasKey(key);

if (hasKey == null || !hasKey) {
throw new MorandiException(SubmitErrorCode.COOKIE_NOT_EXIST);
}

return HandlerInterceptor.super.preHandle(request, response, handler);
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("OnlineJudge 쿠키 존재");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

}
24 changes: 24 additions & 0 deletions src/main/java/swm_nm/morandi/interceptor/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package swm_nm.morandi.interceptor.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import swm_nm.morandi.interceptor.BaekjoonLoginInterceptor;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final BaekjoonLoginInterceptor baekjoonLoginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(baekjoonLoginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(List.of("/submit/cookie","/swagger-ui/**","/v3/api-docs","/swagger-resources/**","/oauths/**"));
}

}

File renamed without changes.
9 changes: 9 additions & 0 deletions src/main/resources/codes/temp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import java.lang.*;

class Main
{
public static void main (String[] args)
{
System.out.println("Hello World!");
}
}
File renamed without changes.
Loading