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

[#11] JWT 설정 #13

Merged
merged 7 commits into from
Jan 9, 2025
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
2 changes: 2 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
24 changes: 15 additions & 9 deletions api/src/main/java/com/whalewatch/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package com.whalewatch.controller;

import com.whalewatch.domain.User;
import com.whalewatch.dto.TokenResponseDto;
import com.whalewatch.dto.UserDto;
import com.whalewatch.mapper.UserMapper;
import com.whalewatch.service.JwtService;
import com.whalewatch.service.UserService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final JwtService jwtService;
private final UserMapper userMapper;

public UserController(UserService userService,UserMapper userMapper) {
public UserController(UserService userService,
UserMapper userMapper,
JwtService jwtService) {
this.userService = userService;
this.userMapper = userMapper;
this.jwtService = jwtService;
}

@PostMapping
Expand All @@ -25,19 +31,19 @@ public UserDto registerUser(@RequestBody UserDto userDto) {
}

@PostMapping("/login")
public UserDto loginUser(@RequestBody UserDto userDto) {
User user = userService.loginUser(userDto.getEmail(),userDto.getPassword());

if (!user.getPassword().equals(userDto.getPassword())){
throw new RuntimeException("Invalid password");
}
public TokenResponseDto loginUser(@RequestBody UserDto userDto) {
// JwtService로 로그인 + 토큰 발급
return jwtService.login(userDto.getEmail(),userDto.getPassword());
}

return userMapper.toDto(user);
@PostMapping("/refresh")
public TokenResponseDto refreshToken(@RequestBody TokenResponseDto tokenDto){
return jwtService.refreshAccessToken(tokenDto.getRefreshToken());
}

@GetMapping("{id}")
public UserDto getUserInfo(@PathVariable int id) {
User user = userService.getUserInfo(id);
return userMapper.toDto(user);
}
}
}
81 changes: 71 additions & 10 deletions api/src/test/java/com/whalewatch/controller/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.whalewatch.domain.User;
import com.whalewatch.dto.TokenResponseDto;
import com.whalewatch.dto.UserDto;
import com.whalewatch.repository.JwtTokenRepository;
import com.whalewatch.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
Expand All @@ -22,6 +26,11 @@

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"jwt.secret-key=zTjEp7AUmDS+bUZKV5OFIVUtFL7EQCMflxiZ3gxpxo0=",
"jwt.access-token-validity-in-seconds=600",
"jwt.refresh-token-validity-in-seconds=1209600"
})
public class UserControllerTest {

@Autowired
Expand All @@ -33,18 +42,27 @@ public class UserControllerTest {
@Autowired
private ObjectMapper objectMapper;

@Autowired
private JwtTokenRepository jwtTokenRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@BeforeEach
void setup() {
jwtTokenRepository.deleteAll();
userRepository.deleteAll();
userRepository.save(new User("[email protected]", "tester", "1234"));

String hashed = passwordEncoder.encode("1234");
userRepository.save(new User("[email protected]", "tester", hashed));
}

@Test
void testRegisterUser() throws Exception {
// given
UserDto request = new UserDto();
request.setEmail("test@test.com");
request.setUsername("tester");
request.setEmail("test2@test.com");
request.setUsername("tester2");
request.setPassword("1234");

// when
Expand All @@ -53,8 +71,8 @@ void testRegisterUser() throws Exception {
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.email").value("test@test.com"))
.andExpect(jsonPath("$.username").value("tester"));
.andExpect(jsonPath("$.email").value("test2@test.com"))
.andExpect(jsonPath("$.username").value("tester2"));

List<User> all = userRepository.findAll();
assertEquals(2, all.size());
Expand All @@ -72,20 +90,63 @@ void testLoginUser() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.username").value("tester"));
.andExpect(jsonPath("$.accessToken").exists())
.andExpect(jsonPath("$.refreshToken").exists());
}

@Test
void testGetUserInfo() throws Exception {
// given
User first = userRepository.findAll().get(0);
User user = userRepository.findAll().get(0);

// 로그인
UserDto loginRequest = new UserDto();
loginRequest.setEmail("[email protected]");
loginRequest.setPassword("1234");

String loginResponse = mockMvc.perform(post("/api/users/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();

TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class);
String accessToken = tokens.getAccessToken();

// when & then
mockMvc.perform(get("/api/users/" + first.getId()))
mockMvc.perform(get("/api/users/" + user.getId())
.header("Authorization", "Bearer " + accessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.username").value("tester"));
}

@Test
void testRefreshToken() throws Exception {
//given
UserDto loginRequest = new UserDto();
loginRequest.setEmail("[email protected]");
loginRequest.setPassword("1234");

//when
String loginResponse = mockMvc.perform(post("/api/users/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();

TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class);
String refreshToken = tokens.getRefreshToken();

//given - accesstoken 재발급
TokenResponseDto refreshRequest = new TokenResponseDto("", refreshToken);

// then
mockMvc.perform(post("/api/users/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(refreshRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").exists())
.andExpect(jsonPath("$.refreshToken").value(refreshToken));
}
}
5 changes: 5 additions & 0 deletions application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ spring:
console:
enabled: true
path: /h2-console

jwt:
secret-key: "${JWT_SECRET_KEY}"
access-token-validity-in-seconds: 600
refresh-token-validity-in-seconds: 1209600
11 changes: 11 additions & 0 deletions common/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}

dependencies {
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// JWT 설정
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
50 changes: 50 additions & 0 deletions common/src/main/java/com/whalewatch/config/JwtAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.whalewatch.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthentication extends OncePerRequestFilter {

private final JwtTokenProvider tokenProvider;

public JwtAuthentication(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}

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

String jwt = resolveToken(request);

if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String email = tokenProvider.getEmailFromToken(jwt);

// DB 조회 후 권한 설정 가능
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(email, null, null);

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
// "Authorization: Bearer <토큰값>" 형태일 때 추출
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
74 changes: 74 additions & 0 deletions common/src/main/java/com/whalewatch/config/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.whalewatch.config;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {
@Value("${jwt.secret-key}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private String secretKey;

@Value("${jwt.access-token-validity-in-seconds}")
private long accessTokenValidity;

@Value("${jwt.refresh-token-validity-in-seconds}")
private long refreshTokenValidity;

public JwtTokenProvider() {
}

// Access Token 생성
public String generateAccessToken(String email) {
Date now = new Date();
Date expiry = new Date(now.getTime() + accessTokenValidity * 1000);

return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

// Refresh Token 생성
public String generateRefreshToken(String email) {
Date now = new Date();
Date expiry = new Date(now.getTime() + refreshTokenValidity * 1000);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh token의 만료시간이랑 access token의 만료시간이 같으면, refresh를 못하지 않을까요? access token으로 통신하다가 access token이 만료되면, refresh token을 써서 갱신하는 방식일 것 같은데요 ...


return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

// 토큰에서 Subject 추출
public String getEmailFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}

// 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// 유효하지 않은 토큰
}
return false;
}
}
Loading
Loading