Skip to content
Open
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
3 changes: 3 additions & 0 deletions naru/umc10th/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ public ApiResponse<AuthResponseDto.SignUpResultDto> signUp(
AuthResponseDto.SignUpResultDto result = authService.signUp(request);
return ApiResponse.onSuccess(UserSuccessCode.USER_JOIN_SUCCESS, result);
}

@Operation(summary = "로그인 API")
@PostMapping("/login")
public ApiResponse<AuthResponseDto.LoginResultDto> login(
@RequestBody @Valid AuthRequestDto.LoginDto request
) {
AuthResponseDto.LoginResultDto result = authService.login(request);
return ApiResponse.onSuccess(UserSuccessCode.USER_LOGIN_SUCCESS, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@

public class AuthRequestDto {

public record LoginDto(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {}

public record SignUpDto(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@ public record SignUpResultDto(
String email,
String name
) {}

@Builder
public record LoginResultDto(
String grantType,
String accessToken,
String refreshToken,
Long accessTokenExpiresIn
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@
public interface AuthService {

AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request);

AuthResponseDto.LoginResultDto login(AuthRequestDto.LoginDto request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
import com.example.umc10th.domain.user.exception.UserException;
import com.example.umc10th.domain.user.exception.code.UserErrorCode;
import com.example.umc10th.domain.user.repository.UserRepository;
import com.example.umc10th.global.security.jwt.JwtUtil;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

Expand All @@ -37,6 +42,8 @@ public class AuthServiceImpl implements AuthService {
private final TermRepository termRepository;
private final UserAgreementRepository userAgreementRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;

@Override
@Transactional
Expand Down Expand Up @@ -71,6 +78,20 @@ public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request)
return AuthConverter.toSignUpResultDto(savedUser);
}

@Override
public AuthResponseDto.LoginResultDto login(AuthRequestDto.LoginDto request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password())
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return AuthResponseDto.LoginResultDto.builder()
.grantType("Bearer")
.accessToken(jwtUtil.createAccessToken(userDetails))
.refreshToken(jwtUtil.createRefreshToken(userDetails))
.accessTokenExpiresIn(jwtUtil.getAccessTokenExpiration())
.build();
}

private void validateRequiredTerms(Map<Long, Boolean> agreementMap) {
List<Term> requiredTerms = termRepository.findAllByIsRequiredTrue();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc10th.global.apiPayload.code.GeneralSuccessCode;
import com.example.umc10th.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -26,9 +28,9 @@ public class UserController {
@Operation(summary = "마이페이지 조회 API")
@GetMapping("/mypage")
public ApiResponse<UserResponseDto.MyPageResultDto> getMyPage(
@RequestParam Long userId
@AuthenticationPrincipal CustomUserDetails userDetails
) {
UserResponseDto.MyPageResultDto result = userService.getMyPage(userId);
UserResponseDto.MyPageResultDto result = userService.getMyPage(userDetails.getUserId());
return ApiResponse.onSuccess(GeneralSuccessCode.OK, result);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import com.example.umc10th.global.security.handler.CustomAuthenticationEntryPoint;
import com.example.umc10th.global.security.handler.CustomAuthenticationFailureHandler;
import com.example.umc10th.global.security.handler.CustomAuthenticationSuccessHandler;
import com.example.umc10th.global.security.jwt.JwtAuthFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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
@RequiredArgsConstructor
Expand All @@ -21,6 +25,7 @@ public class SecurityConfig {
private final CustomAccessDeniedHandler accessDeniedHandler;
private final CustomAuthenticationSuccessHandler authenticationSuccessHandler;
private final CustomAuthenticationFailureHandler authenticationFailureHandler;
private final JwtAuthFilter jwtAuthFilter;

private static final String[] PUBLIC_URLS = {
"/auth/**",
Expand Down Expand Up @@ -53,11 +58,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

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

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.example.umc10th.global.security.handler;

import com.example.umc10th.domain.auth.dto.AuthResponseDto;
import com.example.umc10th.domain.user.exception.code.UserSuccessCode;
import com.example.umc10th.global.security.jwt.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

Expand All @@ -15,10 +18,18 @@
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private final SecurityResponseWriter responseWriter;
private final JwtUtil jwtUtil;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
responseWriter.writeSuccess(response, UserSuccessCode.USER_LOGIN_SUCCESS);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
AuthResponseDto.LoginResultDto result = AuthResponseDto.LoginResultDto.builder()
.grantType("Bearer")
.accessToken(jwtUtil.createAccessToken(userDetails))
.refreshToken(jwtUtil.createRefreshToken(userDetails))
.accessTokenExpiresIn(jwtUtil.getAccessTokenExpiration())
.build();
responseWriter.writeSuccess(response, UserSuccessCode.USER_LOGIN_SUCCESS, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public void writeFailure(HttpServletResponse response, BaseErrorCode code) throw
objectMapper.writeValue(response.getWriter(), ApiResponse.onFailure(code, null));
}

public void writeSuccess(HttpServletResponse response, BaseSuccessCode code) throws IOException {
public <T> void writeSuccess(HttpServletResponse response, BaseSuccessCode code, T result) throws IOException {
response.setStatus(code.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
objectMapper.writeValue(response.getWriter(), ApiResponse.onSuccess(code, null));
objectMapper.writeValue(response.getWriter(), ApiResponse.onSuccess(code, result));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.umc10th.global.security.jwt;

import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc10th.global.security.handler.SecurityResponseWriter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";

private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final SecurityResponseWriter responseWriter;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);

if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}

String token = authorizationHeader.substring(BEARER_PREFIX.length());

if (!jwtUtil.validateToken(token)) {
responseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED);
return;
}

try {
String email = jwtUtil.getEmail(token);

if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
} catch (UsernameNotFoundException | IllegalArgumentException e) {
SecurityContextHolder.clearContext();
responseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.example.umc10th.global.security.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;

@Component
public class JwtUtil {

private final SecretKey secretKey;
private final long accessTokenExpiration;
private final long refreshTokenExpiration;

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

public String createAccessToken(UserDetails userDetails) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpiration);
List<String> authorities = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
String role = authorities.stream()
.findFirst()
.map(authority -> authority.replaceFirst("^ROLE_", ""))
.orElse("");

return Jwts.builder()
.subject(userDetails.getUsername())
.claim("role", role)
.claim("authorities", authorities)
.issuedAt(now)
.expiration(expiration)
.signWith(secretKey)
.compact();
}

public String createRefreshToken(UserDetails userDetails) {
Date now = new Date();
Date expiration = new Date(now.getTime() + refreshTokenExpiration);

return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(now)
.expiration(expiration)
.signWith(secretKey)
.compact();
}

public long getAccessTokenExpiration() {
return accessTokenExpiration;
}

public String getEmail(String token) {
return parseClaims(token).getSubject();
}

public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
9 changes: 8 additions & 1 deletion naru/umc10th/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ spring:
ddl-auto: update
properties:
hibernate:
format_sql: true
format_sql: true

jwt:
token:
secretKey: ${JWT_SECRET_KEY}
expiration:
access: 3600000
refresh: 1209600000