Skip to content

Commit

Permalink
feat: (#1) Jwt 필터 추가 및 Secuirty 필터 설정
Browse files Browse the repository at this point in the history
 - Jwt 생성을 위한 Secret Key 등록
 - /와 같은 health URI에 대해서는 Allow
 - /login/** 에는 Login 필터만 적용
 - /member/** 경우에는 Jwt 필터 적용
  • Loading branch information
BEMELON committed Feb 15, 2023
1 parent 10fc7c6 commit cd8c81d
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 10 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ dependencies {
implementation 'org.hibernate:hibernate-core:5.6.14.Final'
implementation 'mysql:mysql-connector-java:8.0.32'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2' // JWT
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' // JWT
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' // JWT
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/com/tune/server/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tune.server.filter.ExceptionHandlerFilter;
import com.tune.server.filter.JwtAuthenticationFilter;
import com.tune.server.filter.KakaoLoginFilter;
import com.tune.server.service.member.MemberService;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -30,7 +31,8 @@ public WebSecurityCustomizer webSecurityCustomizer() {
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/h2-console/**"
"/h2-console/**",
"/"
);
}

Expand All @@ -39,11 +41,12 @@ protected DefaultSecurityFilterChain configure(HttpSecurity http) throws Excepti
return http
.cors().disable()
.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin().disable()
.authorizeRequests().antMatchers("/login/**").authenticated().and()
.addFilterBefore(new KakaoLoginFilter("/login/kakao", objectMapper, memberService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new ExceptionHandlerFilter(), KakaoLoginFilter.class)
.authorizeRequests().antMatchers("/member/**").authenticated().and()
.addFilterAfter(new JwtAuthenticationFilter(objectMapper, memberService), KakaoLoginFilter.class)
.build();
}

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/tune/server/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tune.server.controller;

import com.tune.server.domain.Member;
import com.tune.server.dto.response.MemberResponse;
import com.tune.server.service.member.MemberService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;

@RestController
@AllArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/member")
public ResponseEntity<MemberResponse> getMember(@ApiIgnore Authentication authentication) {
return ResponseEntity.ok(MemberResponse.of((Member) authentication.getPrincipal()));
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/tune/server/dto/response/MemberResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tune.server.dto.response;

import com.tune.server.domain.Member;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Getter
@Builder
@ToString
public class MemberResponse {
private Long id;
private String name;

public static MemberResponse of(Member member) {
return MemberResponse.builder()
.id(member.getId())
.name(member.getName())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

public class InvalidTokenException extends ErrorResponse {
public InvalidTokenException(String message) {
super(message, HttpStatus.UNAUTHORIZED.value());
super(message, HttpStatus.BAD_REQUEST.value());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tune.server.exceptions.login;

import com.tune.server.exceptions.ErrorResponse;
import org.springframework.http.HttpStatus;

public class TokenExpiredException extends ErrorResponse {
public TokenExpiredException(String message) {
super(message, HttpStatus.UNAUTHORIZED.value());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tune.server.exceptions.login;

import com.tune.server.exceptions.ErrorResponse;
import org.springframework.http.HttpStatus;

public class TokenNotFoundException extends ErrorResponse {
public TokenNotFoundException(String message) {
super(message, HttpStatus.FORBIDDEN.value());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
errorResponse.put("message", e.getMessage());
errorResponse.put("status", e.getStatus());

response.setStatus(e.getStatus());
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/com/tune/server/filter/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.tune.server.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tune.server.domain.Member;
import com.tune.server.exceptions.login.InvalidTokenException;
import com.tune.server.exceptions.login.TokenExpiredException;
import com.tune.server.exceptions.login.TokenNotFoundException;
import com.tune.server.service.member.MemberService;
import com.tune.server.util.JwtUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;

@AllArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private ObjectMapper objectMapper;
private MemberService memberService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer "))
throw new TokenNotFoundException("JWT Token does not begin with Bearer String with URL : " + request.getRequestURI());


String token = authorizationHeader.split(" ")[1];
if (!JwtUtil.isValidJwt(token))
throw new InvalidTokenException("JWT Token is not valid with Jwt : " + token);

if (JwtUtil.isExpired(token, Date.from(LocalDateTime.now().atZone(ZoneId.of("Asia/Seoul")).toInstant())))
throw new TokenExpiredException("JWT Token is expired");

// Member 식별
Member member = JwtUtil.getMemberFromJwt(token, objectMapper);

// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(member, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));

// UserDetail을 통해 인증된 사용자 정보를 SecurityContext에 저장
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
16 changes: 15 additions & 1 deletion src/main/java/com/tune/server/filter/KakaoLoginFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.tune.server.dto.response.FullTokenResponse;
import com.tune.server.exceptions.login.InvalidTokenException;
import com.tune.server.exceptions.login.KakaoServerException;
import com.tune.server.exceptions.login.TokenNotFoundException;
import com.tune.server.service.member.MemberService;
import com.tune.server.util.JwtUtil;
import org.springframework.http.*;
Expand Down Expand Up @@ -41,7 +42,7 @@ public KakaoLoginFilter(String signupUri, ObjectMapper objectMapper, MemberServi
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (SIGNUP_URI.equals(request.getRequestURI()) && request.getMethod().equals("POST")) {
KakaoTokenRequest kakaoTokenRequest = objectMapper.readValue(request.getInputStream(), KakaoTokenRequest.class);
KakaoTokenRequest kakaoTokenRequest = getKakaoToken(request);
String refreshToken = kakaoTokenRequest.getRefresh_token();
String accessToken = kakaoTokenRequest.getAccess_token();

Expand Down Expand Up @@ -85,6 +86,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
filterChain.doFilter(request, response);
}

private KakaoTokenRequest getKakaoToken(HttpServletRequest request) {
try {
KakaoTokenRequest kakaoTokenRequest = objectMapper.readValue(request.getInputStream(), KakaoTokenRequest.class);
if (kakaoTokenRequest.getAccess_token() == null || kakaoTokenRequest.getRefresh_token() == null) {
throw new TokenNotFoundException("access_token 또는 refresh_token이 없습니다.");
}

return kakaoTokenRequest;
} catch (IOException e) {
throw new TokenNotFoundException("카카오 토큰을 조회하는 중에 오류가 발생했습니다.");
}
}

private KakaoUserInfo getKaKaoUserInfo(String accessToken) throws JsonProcessingException {
// KAKAO_USER_INFO_URL로 accessToken을 보내서 회원 정보를 받아온다.
RestTemplate restTemplate = new RestTemplate();
Expand Down
70 changes: 68 additions & 2 deletions src/main/java/com/tune/server/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,86 @@
package com.tune.server.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tune.server.domain.Member;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Component
public class JwtUtil {

// JWT_SECRET_KEY is set in application.yml
public static String JWT_SECRET_KEY;

// refresh token expires in 6 months
public static final int REFRESH_TOKEN_EXPIRES_MONTH = 6;

// access token expires in 12 hours
public static final int ACCESS_TOKEN_EXPIRE_MINUTE = 12 * 60;

@Value("${external.jwt.secret}")
public void setKey(String key) {
JWT_SECRET_KEY = key;
}

public static Member getMemberFromJwt(String token, ObjectMapper objectMapper) {
Map<String, Object> claims = Jwts.parserBuilder()
.setSigningKey(JWT_SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();

return objectMapper.convertValue(claims, Member.class);
}

public static String generateJwt(Member member) {
return "jwt";
Date now = Date.from(LocalDateTime.now().atZone(ZoneId.of("Asia/Seoul")).toInstant());
Date accessTokenExpiredDate = Date.from(LocalDateTime.now().plusMinutes(ACCESS_TOKEN_EXPIRE_MINUTE).atZone(ZoneId.of("Asia/Seoul")).toInstant());

Map<String, Object> claims = new HashMap<>();
claims.put("id", member.getId());
claims.put("name", member.getName());

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(accessTokenExpiredDate)
.signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY)
.compact();
}

public static boolean isValidJwt(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(JWT_SECRET_KEY)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}

public static boolean isExpired(String token, Date date) {
Date expiredDate = Jwts.parserBuilder()
.setSigningKey(JWT_SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();

return expiredDate.before(date);
}

public static String generateRefreshToken() {
return "refreshToken";
return UUID.randomUUID().toString().replace("-", "");
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ spring:
enabled: always
server:
port: 8081

external:
jwt:
secret: ${JWT_SECRET}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.tune.server.domain.Member;
import com.tune.server.domain.MemberProvider;
import com.tune.server.dto.kakao.KakaoUserInfo;
import com.tune.server.enums.ProviderEnum;
import com.tune.server.repository.MemberProviderRepository;
import com.tune.server.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
Expand Down Expand Up @@ -44,7 +43,7 @@ void isExistMember_success() {
// when
MemberProvider memberProvider = MemberProvider.builder()
.id(kakaoUserInfo.getId())
.provider(ProviderEnum.KAKAO)
.provider("KAKAO")
.build();
memberProviderRepository.save(memberProvider);

Expand Down
Loading

0 comments on commit cd8c81d

Please sign in to comment.