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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

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

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}


Expand Down
4 changes: 4 additions & 0 deletions src/main/generated/umc/study/domain/QUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ public class QUser extends EntityPathBase<User> {

public final StringPath name = createString("name");

public final StringPath password = createString("password");

public final StringPath phoneNumber = createString("phoneNumber");

public final NumberPath<Integer> point = createNumber("point", Integer.class);

public final ListPath<Review, QReview> reviewList = this.<Review, QReview>createList("reviewList", Review.class, QReview.class, PathInits.DIRECT2);

public final EnumPath<umc.study.domain.enums.Role> role = createEnum("role", umc.study.domain.enums.Role.class);

public final EnumPath<umc.study.domain.enums.UserStatus> status = createEnum("status", umc.study.domain.enums.UserStatus.class);

//inherited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public QUserPrefer(PathMetadata metadata, PathInits inits) {

public QUserPrefer(Class<? extends UserPrefer> type, PathMetadata metadata, PathInits inits) {
super(type, metadata, inits);
this.foodCategory = inits.isInitialized("foodCategory") ? new umc.study.domain.QFoodCategory(forProperty("foodCategory")) : null;
this.foodCategory = inits.isInitialized("foodCategory") ? new umc.study.domain.QFoodCategory(forProperty("foodCategory"), inits.get("foodCategory")) : null;
this.user = inits.isInitialized("user") ? new umc.study.domain.QUser(forProperty("user")) : null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public enum ErrorStatus implements BaseErrorCode {
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "no user found."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "nickname is required."),
INVALID_GENDER(HttpStatus.BAD_REQUEST, "USER4003", "Invalid gender."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "USER4003", "password is incorrect."),
DUPLICATE_JOIN_REQUEST(HttpStatus.BAD_REQUEST, "USER4004", "already exists with the same email address."),

INVALID_TOKEN(HttpStatus.BAD_REQUEST, "USER4005", "Invalid token."),

// 게시글 에러
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "no articles."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package umc.study.apiPayload.exceptition;
package umc.study.apiPayload.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package umc.study.apiPayload.exceptition;
package umc.study.apiPayload.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class FoodCategoryHandler extends GeneralException {
public FoodCategoryHandler(BaseErrorCode errorCode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class LocationHandler extends GeneralException {
public LocationHandler(BaseErrorCode errorCode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class MissionHandler extends GeneralException {
MissionHandler(BaseErrorCode errorCode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class StoreHandler extends GeneralException {
public StoreHandler(BaseErrorCode errorCode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class TempHandler extends GeneralException {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package umc.study.apiPayload.exceptition.handler;
package umc.study.apiPayload.exception.handler;

import umc.study.apiPayload.code.BaseErrorCode;
import umc.study.apiPayload.exceptition.GeneralException;
import umc.study.apiPayload.exception.GeneralException;

public class UserHandler extends GeneralException {
public UserHandler(BaseErrorCode errorCode) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/umc/study/config/properties/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package umc.study.config.properties;

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

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

@Getter
@Setter
public static class Expiration{
private Long access;
// TODO: refreshToken
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package umc.study.config.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import umc.study.domain.User;
import umc.study.repository.UserRepository.UserRepository;

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info(">>> 사용자 정보 로딩: {}", username);

User user = (User) userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다: " + username));

log.info(">>> 로드된 사용자 정보 - ID: {}, 이메일: {}, 역할: {}",
user.getId(), user.getEmail(), user.getRole());

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

import lombok.RequiredArgsConstructor;
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;
import umc.study.config.security.jwt.JwtAuthenticationFilter;
import umc.study.config.security.jwt.JwtTokenProvider;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(
(requests) -> requests
.requestMatchers("/", "/users/join", "/users/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.csrf()
.disable()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package umc.study.config.security.jwt;

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.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import umc.study.config.properties.Constants;

import java.io.IOException;

@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 = resolveToken(request);

if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(Constants.AUTH_HEADER);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) {
return bearerToken.substring(Constants.TOKEN_PREFIX.length());
}
return null;
}
}
84 changes: 84 additions & 0 deletions src/main/java/umc/study/config/security/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package umc.study.config.security.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
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 umc.study.apiPayload.code.status.ErrorStatus;
import umc.study.apiPayload.exception.handler.UserHandler;
import umc.study.config.properties.Constants;
import umc.study.config.properties.JwtProperties;

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

@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 boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

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);

User principal = new User(email, "", Collections.singleton(() -> role));
return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities());
}

public static String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(Constants.AUTH_HEADER);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) {
return bearerToken.substring(Constants.TOKEN_PREFIX.length());
}
return null;
}

public Authentication extractAuthentication(HttpServletRequest request){
String accessToken = resolveToken(request);
if(accessToken == null || !validateToken(accessToken)) {
throw new UserHandler(ErrorStatus.INVALID_TOKEN);
}
return getAuthentication(accessToken);
}
}
16 changes: 7 additions & 9 deletions src/main/java/umc/study/converter/StoreConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,15 @@ public static StoreResponseDto.ReviewPreViewDTO reviewPreViewDTO(Review review){
.build();
}
public static StoreResponseDto.ReviewPreViewListDTO reviewPreViewListDTO(Page<Review> reviewList){

List<StoreResponseDto.ReviewPreViewDTO> reviewPreViewDTOList = reviewList.stream()
.map(StoreConverter::reviewPreViewDTO).collect(Collectors.toList());
Page<StoreResponseDto.ReviewPreViewDTO> reviewPreViewDTOPage = reviewList.map(StoreConverter::reviewPreViewDTO);

return StoreResponseDto.ReviewPreViewListDTO.builder()
.isLast(reviewList.isLast())
.isFirst(reviewList.isFirst())
.totalPage(reviewList.getTotalPages())
.totalElements(reviewList.getTotalElements())
.listSize(reviewPreViewDTOList.size())
.reviewList(reviewPreViewDTOList)
.isLast(reviewPreViewDTOPage.isLast())
.isFirst(reviewPreViewDTOPage.isFirst())
.totalPage(reviewPreViewDTOPage.getTotalPages())
.totalElements(reviewPreViewDTOPage.getTotalElements())
.listSize(reviewPreViewDTOPage.getContent().size())
.reviewList(reviewPreViewDTOPage.getContent())
.build();
}

Expand Down
Loading