Skip to content

Commit

Permalink
Merge pull request #83 from alzzaipo/refresh-token
Browse files Browse the repository at this point in the history
리프레시 토큰 도입
  • Loading branch information
csct3434 authored Dec 18, 2023
2 parents 11fa49a + 5f99920 commit 1f4ee4c
Show file tree
Hide file tree
Showing 24 changed files with 437 additions and 212 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

implementation 'mysql:mysql-connector-java'

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/alzzaipo/common/Uid.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public Uid(Long uid) {
selfValidate();
}

public Uid(String uid) {
this.uid = Long.parseLong(uid);
selfValidate();
}

private void selfValidate() {
if (uid == null) {
throw new CustomException(HttpStatus.BAD_REQUEST, "UID 오류 : null");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.alzzaipo.common.jwt;
package com.alzzaipo.common.config;

import com.alzzaipo.common.LoginType;
import com.alzzaipo.common.MemberPrincipal;
import com.alzzaipo.common.Uid;
import com.alzzaipo.common.jwt.JwtUtil;
import com.alzzaipo.member.adapter.out.persistence.member.MemberJpaEntity;
import com.alzzaipo.member.adapter.out.persistence.member.MemberRepository;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -43,7 +42,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String token = resolveTokenFromAuthorizationHeader(request);
if (token == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token Not Found");
response.getWriter().write("Missing Token");
filterChain.doFilter(request, response);
return;
}
Expand All @@ -57,19 +56,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
SecurityContextHolder.getContext().setAuthentication(authentication);

filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token has been expired");
} catch (SignatureException | BadCredentialsException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid Token");
} catch (UsernameNotFoundException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("User Not Found");
} catch (Exception e) {
logger.error(e.getMessage());
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Authentication Error");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid Token");
}
}

Expand Down Expand Up @@ -97,17 +87,15 @@ private MemberPrincipal createPrincipal(String token) {

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
List<String> excludePath = Arrays.asList(
List<String> whitelist = Arrays.asList(
"/member/verify-account-id",
"/member/verify-email",
"/member/register",
"/member/login",
"/ipo",
"/email",
"/oauth/kakao/login");
"/login");

String path = request.getRequestURI();

return excludePath.stream().anyMatch(path::startsWith);
return whitelist.stream().anyMatch(path::startsWith);
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/alzzaipo/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.alzzaipo.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());

return redisTemplate;
}
}
3 changes: 1 addition & 2 deletions src/main/java/com/alzzaipo/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.alzzaipo.common.config;

import com.alzzaipo.common.jwt.JwtFilter;
import com.alzzaipo.member.domain.member.Role;
import com.alzzaipo.member.adapter.out.persistence.member.MemberRepository;
import java.util.List;
Expand Down Expand Up @@ -42,7 +41,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/member/login",
"/ipo/**",
"/email/**",
"/oauth/kakao/login").permitAll()
"/login/**").permitAll()
.requestMatchers("/scraper").hasRole(Role.ADMIN.name())
.anyRequest().authenticated());

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/alzzaipo/common/jwt/JwtProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
public class JwtProperties {

private String secretKey;
private Long expirationTimeMillis;
private Long accessTokenExpirationTimeMillis;
private Long refreshTokenExpirationTimeMillis;
}
39 changes: 28 additions & 11 deletions src/main/java/com/alzzaipo/common/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,10 @@ private JwtUtil(JwtProperties jwtProperties) {
JwtUtil.jwtProperties = jwtProperties;
}

public static String createToken(Uid memberUID, LoginType loginType) {
Claims claims = Jwts.claims();
claims.setSubject(memberUID.toString());
claims.put("loginType", loginType.name());

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationTimeMillis()))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
public static TokenInfo createToken(Uid memberId, LoginType loginType) {
String accessToken = generateToken(memberId, loginType, jwtProperties.getAccessTokenExpirationTimeMillis());
String refreshToken = generateToken(null, loginType, jwtProperties.getRefreshTokenExpirationTimeMillis());
return new TokenInfo(accessToken, refreshToken);
}

public static Uid getMemberUID(String token) {
Expand All @@ -44,6 +37,30 @@ public static LoginType getLoginType(String token) {
return LoginType.valueOf((getClaims(token).get("loginType", String.class)));
}

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

private static String generateToken(Uid memberId, LoginType loginType, long expirationTimeMillis) {
Claims claims = Jwts.claims();
claims.put("loginType", loginType.name());
if(memberId != null) {
claims.setSubject(memberId.toString());
}

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expirationTimeMillis))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}

private static Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/alzzaipo/common/jwt/TokenInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.alzzaipo.common.jwt;

import lombok.Getter;

@Getter
public class TokenInfo {

private final String accessToken;
private final String RefreshToken;

public TokenInfo(String accessToken, String refreshToken) {
this.accessToken = accessToken;
RefreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.alzzaipo.member.adapter.in.web;

import com.alzzaipo.common.jwt.TokenInfo;
import com.alzzaipo.member.adapter.in.web.dto.LocalLoginWebRequest;
import com.alzzaipo.member.adapter.in.web.dto.RefreshTokenDto;
import com.alzzaipo.member.adapter.in.web.dto.TokenResponse;
import com.alzzaipo.member.application.port.in.RefreshTokenUseCase;
import com.alzzaipo.member.application.port.in.account.local.LocalLoginUseCase;
import com.alzzaipo.member.application.port.in.dto.AuthorizationCode;
import com.alzzaipo.member.application.port.in.dto.LocalLoginCommand;
import com.alzzaipo.member.application.port.in.dto.LoginResult;
import com.alzzaipo.member.application.port.in.oauth.KakaoLoginUseCase;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/login")
@RequiredArgsConstructor
public class LoginController {

private final LocalLoginUseCase localLoginUseCase;
private final KakaoLoginUseCase kakaoLoginUseCase;
private final RefreshTokenUseCase refreshTokenUseCase;

@PostMapping("/local")
public ResponseEntity<TokenResponse> login(@Valid @RequestBody LocalLoginWebRequest dto) {
LocalLoginCommand localLoginCommand = new LocalLoginCommand(
dto.getAccountId(),
dto.getAccountPassword());

LoginResult loginResult = localLoginUseCase.handleLocalLogin(localLoginCommand);

if (loginResult.isSuccess()) {
TokenResponse tokenResponse = TokenResponse.build(loginResult.getTokenInfo());
return ResponseEntity.ok(tokenResponse);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

@GetMapping("/kakao")
public ResponseEntity<TokenResponse> kakaoLogin(@RequestParam("code") String authCode) {
AuthorizationCode authorizationCode = new AuthorizationCode(authCode);

LoginResult loginResult = kakaoLoginUseCase.handleLogin(authorizationCode);

if (loginResult.isSuccess()) {
TokenResponse tokenResponse = TokenResponse.build(loginResult.getTokenInfo());
return ResponseEntity.ok(tokenResponse);
}
return ResponseEntity.badRequest().build();
}

@PostMapping("/refresh-token")
public ResponseEntity<TokenResponse> refreshToken(@Valid @RequestBody RefreshTokenDto dto) {
TokenInfo tokenInfo = refreshTokenUseCase.refresh(dto.getRefreshToken());
TokenResponse tokenResponse = TokenResponse.build(tokenInfo);
return ResponseEntity.ok(tokenResponse);
}
}
Loading

0 comments on commit 1f4ee4c

Please sign in to comment.