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
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ dependencies {

// 스웨거 의존성
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'

// 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'

//Spring Boot Security
implementation 'org.springframework.boot:spring-boot-starter-security'
}

tasks.named('test') {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/umc/apiPayload/code/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public enum ErrorStatus implements BaseErrorCode {
// 멤버 관련 에러
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."),
EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4003", "해당 이메일의 사용자가 없습니다."),
INVALID_PASSWORD(HttpStatus.NOT_FOUND, "USER4004", "비밀번호가 일치하지 않습니다."),

// 예시
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),
Expand All @@ -41,6 +43,9 @@ public enum ErrorStatus implements BaseErrorCode {
ALREADY_CHALLENGE(HttpStatus.BAD_REQUEST, "MISSION4002", "이미 수행 중인 미션입니다."),
USER_MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION4003", "수행 중인 미션이 존재하지 않습니다."),

// 토큰 관련 에러
INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 유효하지 않습니다."),

// 페이징 관련 에러
PAGE_NOT_VALID(HttpStatus.BAD_REQUEST, "PAGE4001", "페이징 번호가 유효하지 않습니다.");

Expand Down
9 changes: 9 additions & 0 deletions src/main/java/umc/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
Expand All @@ -13,6 +15,13 @@
import io.swagger.v3.oas.models.servers.Server;

@Configuration
@io.swagger.v3.oas.annotations.security.SecurityScheme(
name = "JWT Token",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER
)
public class SwaggerConfig {

@Bean
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/umc/config/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package umc.config.jwt;

import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = JwtTokenProvider.resolveToken(request);
System.out.println(token);
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
103 changes: 103 additions & 0 deletions src/main/java/umc/config/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package umc.config.jwt;

import java.security.Key;
import java.util.Collections;
import java.util.Date;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import umc.apiPayload.code.status.ErrorStatus;
import umc.apiPayload.exception.GeneralException;
import umc.config.properties.Constants;
import umc.config.properties.JwtProperties;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private final JwtProperties jwtProperties;

private Key getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes());
}

// 토큰 생성
public String generateToken(Authentication authentication) {
String email = authentication.getName();

return Jwts.builder()
.setSubject(email) // 식별값
.claim("role", authentication.getAuthorities().iterator().next().getAuthority()) // 권한 등 추가 정보
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess()))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}

// 리프레시 토큰 생성
public String generateRefreshToken() {
return Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getRefresh()))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}

// Token 유효성 판단
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

// HTTP Request로부터 Token 추출 및 Authentication 반환
public Authentication extractAuthentication(HttpServletRequest request){
String accessToken = resolveToken(request);
if(accessToken == null || !validateToken(accessToken)) {
throw new GeneralException(ErrorStatus.INVALID_TOKEN);
}
return getAuthentication(accessToken);
}

// Request 로부터 토큰 추출
public static String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(Constants.AUTH_HEADER);
System.out.println(bearerToken);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) {
return bearerToken.substring(Constants.TOKEN_PREFIX.length());
}
return null;
}

// 토큰으로 부터 Authentication 추출
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();

String email = claims.getSubject();
String role = claims.get("role", String.class);

// org.springframework.security.core.userdetails.User
User principal = new User(email, "", Collections.singleton(() -> role));
return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities());
}
}
6 changes: 6 additions & 0 deletions src/main/java/umc/config/properties/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package umc.config.properties;

public final class Constants {
public static final String AUTH_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
}
23 changes: 23 additions & 0 deletions src/main/java/umc/config/properties/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package umc.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
@Getter
@Setter
@ConfigurationProperties("jwt.token")
public class JwtProperties {
private String secretKey="";
private Expiration expiration;

@Getter
@Setter
public static class Expiration{
private Long access;
private Long refresh;
}
}
30 changes: 30 additions & 0 deletions src/main/java/umc/config/security/CustomUserDetailsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package umc.config.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import umc.apiPayload.code.status.ErrorStatus;
import umc.apiPayload.exception.GeneralException;
import umc.domain.User;
import umc.repository.UserRepository.UserRepository;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new GeneralException(ErrorStatus.EMAIL_NOT_FOUND));

return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
47 changes: 47 additions & 0 deletions src/main/java/umc/config/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package umc.config.security;

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.annotation.web.configurers.AbstractHttpConfigurer;
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;

import lombok.RequiredArgsConstructor;
import umc.config.jwt.JwtAuthenticationFilter;
import umc.config.jwt.JwtTokenProvider;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JSESSIONID 발급 X
)
.authorizeHttpRequests(
(requests) -> requests
.requestMatchers("/", "/users/signup", "/users/login", "/swagger-ui/**", "/v3/api-docs/**", "/users/reissue", "/auth/login/kakao/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN") // 인가 필요
.anyRequest().authenticated() // 인증 필요
)
.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
24 changes: 24 additions & 0 deletions src/main/java/umc/controller/OAuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package umc.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import umc.apiPayload.ApiResponse;
import umc.dto.UserResponseDto;
import umc.service.AuthService;

@RestController
@RequiredArgsConstructor
public class OAuthController {

private final AuthService authService;

@GetMapping("/auth/login/kakao")
public ResponseEntity<ApiResponse<UserResponseDto.LoginResultDTO>> kakaoLogin(@RequestParam("code") String accessCode) {
UserResponseDto.LoginResultDTO response = authService.oAuthLogin(accessCode);
return ResponseEntity.ok(ApiResponse.onSuccess(response));
}
}
39 changes: 0 additions & 39 deletions src/main/java/umc/controller/UserController.java

This file was deleted.

Loading