Skip to content

Commit

Permalink
JWT Token
Browse files Browse the repository at this point in the history
  • Loading branch information
namyoonhyeok committed Jan 17, 2025
1 parent a937cba commit af1454b
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 49 deletions.
12 changes: 11 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Spring Security
implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// JWT Token
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('test') {
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/example/digger/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
package com.example.digger.config;

import com.example.digger.jwt.JwtAuthenticationFilter;
import com.example.digger.jwt.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;

public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityContext(securityContext -> securityContext.requireExplicitSave(false)) // 보안 컨텍스트 관리
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // JWT는 Stateless
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/user/join", "/api/user/login", "/api/home").permitAll() // 인증 없이 접근 가능
.anyRequest().authenticated() // 나머지는 인증 필요
)
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가

return http.build();
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/example/digger/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.digger.jwt;

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

import java.io.IOException;
import java.util.Collections;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);

// Request로부터 전달된 토큰에 대한 유효성 검사를 진행한 후, 유효한 토큰일 경우 토큰에 있는 정보를 바탕으로 Authentication을 생성한다.
if (token != null && jwtTokenProvider.validateToken(token)) {
String email = jwtTokenProvider.getEmailFromToken(token);
Authentication authentication = new UsernamePasswordAuthenticationToken(email, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String getTokenFromRequest(HttpServletRequest request) {
// Request의 Authorization 헤더 값을 가져와 거기에서 토큰 값을 추출한다.
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
64 changes: 64 additions & 0 deletions src/main/java/com/example/digger/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.digger.jwt;

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

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;


@Component
public class JwtTokenProvider {

private final Key signingKey;
private final long accessTokenExpiration;
private final long refreshTokenExpiration;

public JwtTokenProvider(@Value("${jwt.secret}") String secretKey,
@Value("${jwt.access-token-expiration}") long accessTokenExpiration,
@Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration) {
this.signingKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
this.accessTokenExpiration = accessTokenExpiration;
this.refreshTokenExpiration = refreshTokenExpiration;
}

public String generateAccessToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}

public String generateRefreshToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpiration))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}

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

public String getEmailFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
63 changes: 29 additions & 34 deletions src/main/java/com/example/digger/user/UserController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.example.digger.user;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/user")
public class UserController {

Expand All @@ -18,38 +16,35 @@ public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/register")
public String showRegisterForm(Model model) {
model.addAttribute("userDTO", new UserDTO());
return "register";
}

@PostMapping("/register")
public String registerUser(@ModelAttribute UserDTO userDTO, RedirectAttributes redirectAttributes) {
boolean isRegistered = userService.registerUser(userDTO);

if (!isRegistered) {
redirectAttributes.addFlashAttribute("error", "이미 등록된 이메일입니다.");
return "redirect:/api/user/register";
@PostMapping("/join")
public ResponseEntity<?> registerUser(@RequestBody UserDTO userDTO) {
try {
String accessToken = userService.joinUser(userDTO);
return ResponseEntity.ok(Map.of("message", "회원가입 성공", "accessToken", accessToken));
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("error", e.getMessage()));
}
return "redirect:/api/user/login";
}

@GetMapping("/login")
public String showLoginForm(Model model) {
model.addAttribute("userDTO", new UserDTO());
return "login";
@PostMapping("/login")
public ResponseEntity<?> loginUser(@RequestBody UserDTO userDTO) {
try {
Map<String, String> tokens = userService.login(userDTO.getEmail(), userDTO.getPassword());
return ResponseEntity.ok(tokens); // accessToken과 refreshToken 반환
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", e.getMessage()));
}
}

@PostMapping("/login")
public String loginUser(@ModelAttribute UserDTO userDTO,RedirectAttributes redirectAttributes, Model model) {
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> request) {
try {
User user = userService.login(userDTO.getEmail(), userDTO.getPassword());
model.addAttribute("user", user);
return "redirect:/api/home";
String email = request.get("email");
String refreshToken = request.get("refreshToken");
String newAccessToken = userService.refreshAccessToken(email, refreshToken);
return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
} catch (RuntimeException e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
return "redirect:/api/user/login";
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", e.getMessage()));
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/example/digger/user/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);
Optional<User> findByEmail(String email);
}
51 changes: 40 additions & 11 deletions src/main/java/com/example/digger/user/UserService.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,72 @@
package com.example.digger.user;

import com.example.digger.jwt.JwtTokenProvider;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class UserService {

private final UserRepository userRepository;

private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;

// 리프레시 토큰 저장소 (데이터베이스 또는 메모리로 변경 가능)
private final Map<String, String> refreshTokenStore = new ConcurrentHashMap<>();

public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}

public boolean registerUser(UserDTO userDTO) {
public String joinUser(UserDTO userDTO) {
if(userRepository.findByEmail(userDTO.getEmail()).isPresent()){
return false;
throw new RuntimeException("이미 등록된 이메일입니다.");
}

User user = new User();
user.setEmail(userDTO.getEmail());
user.setName(userDTO.getName());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));

userRepository.save(user);
return true;

return jwtTokenProvider.generateAccessToken(user.getEmail());
}

public User login(String email, String password) {
Optional<User> userOpt = userRepository.findByEmail(email);
public Map<String, String> login(String email, String password) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("이메일 또는 비밀번호가 잘못되었습니다."));

if (userOpt.isEmpty() || !passwordEncoder.matches(password, userOpt.get().getPassword())) {
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("이메일 또는 비밀번호가 잘못되었습니다.");
}

return userOpt.get();
String accessToken = jwtTokenProvider.generateAccessToken(email);
String refreshToken = jwtTokenProvider.generateRefreshToken(email);

// 리프레시 토큰 저장
refreshTokenStore.put(email, refreshToken);

Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);

return tokens;
}

public String refreshAccessToken(String email, String refreshToken) {
String storedRefreshToken = refreshTokenStore.get(email);

if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
throw new RuntimeException("리프레시 토큰이 유효하지 않습니다.");
}

// 새 액세스 토큰 발급
return jwtTokenProvider.generateAccessToken(email);
}
}
7 changes: 7 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ spring:
url: jdbc:mysql://localhost:3306/digger
username: root
password: yoonheuk7723
thymeleaf:
cache: false

jwt:
secret: XyP3j@2yH8lK7oR9cN#fLz^aWqMvTb&4pQgYxDs%Z!
access-token-expiration: 900000 # 액세스 토큰 만료 시간 (15분 = 900,000ms)
refresh-token-expiration: 604800000 # 리프레시 토큰 만료 시간 (7일 = 7 * 24 * 60 * 60 * 1000ms)
Loading

0 comments on commit af1454b

Please sign in to comment.