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 @@ -38,6 +38,18 @@ dependencies {
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// 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 security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// thymeleaf
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/umc/apiPayload/code/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ public enum ErrorStatus implements BaseErrorCode {
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

// 인증 관련 에러
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "COMMON401", "토큰이 틀립니다."),

// 멤버 관려 에러
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4003", "비밀번호가 틀립니다"),

// 식당 관련 에러
RESTAURANT_NOT_FOUND(HttpStatus.BAD_REQUEST, "RESTAURANT_4001", "식당이 없습니다."),
Expand All @@ -33,6 +36,7 @@ public enum ErrorStatus implements BaseErrorCode {
// 미션 관련 에러
MISSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "MISSION4001", "미션이 존재하지 않습니다"),
MISSION_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MISSION4002", "해당 미션을 수행 중 또는 수행 완료했습니다."),
USERMISSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "MISSION4003", "유저 미션이 존재하지 않습니다."),

// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package umc.apiPayload.exception.handler;

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

public class UserHandler extends GeneralException {

public UserHandler(BaseErrorCode errorCode) {super(errorCode);}
}
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 ";
}
22 changes: 22 additions & 0 deletions src/main/java/umc/config/properties/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package umc.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
}
}
28 changes: 28 additions & 0 deletions src/main/java/umc/config/security/CustomUserDetailsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package umc.config.security;

import lombok.RequiredArgsConstructor;
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.domain.User;
import umc.repository.user.UserRepository;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

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

return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
67 changes: 67 additions & 0 deletions src/main/java/umc/config/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package umc.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.config.security.jwt.JwtAuthenticationFilter;
import umc.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(csrf -> csrf.disable())
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home", "/signup","/users/signup", "/css/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll()
)
.logout((logout) -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
44 changes: 44 additions & 0 deletions src/main/java/umc/config/security/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package umc.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.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;
}
}

85 changes: 85 additions & 0 deletions src/main/java/umc/config/security/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package umc.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.apiPayload.code.status.ErrorStatus;
import umc.apiPayload.exception.handler.UserHandler;
import umc.config.properties.Constants;
import umc.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);
}
}

64 changes: 64 additions & 0 deletions src/main/java/umc/converter/MissionConverter.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package umc.converter;

import org.springframework.data.domain.Page;
import umc.converter.user.UserMissionConverter;
import umc.domain.Location;
import umc.domain.Mission;
import umc.domain.Restaurant;
import umc.domain.Review;
import umc.domain.mapping.UserMission;
import umc.web.dto.mission.MissionRequestDTO;
import umc.web.dto.mission.MissionResponseDTO;
import umc.web.dto.restaurant.RestaurantRequestDTO;
import umc.web.dto.review.ReviewResponseDTO;

import java.util.List;
import java.util.stream.Collectors;

public class MissionConverter {

Expand All @@ -18,4 +27,59 @@ public static Mission toMission(MissionRequestDTO.createMissionDTO request, Rest
.location(restaurant.getLocation())
.build();
}

public static MissionResponseDTO.MissionDTO toMissionDTO(Mission mission){

return MissionResponseDTO.MissionDTO.builder()
.missionName(mission.getName())
.contents(mission.getContents())
.points(mission.getPoints())
.restaurantId(mission.getRestaurant().getId())
.createdAt(mission.getCreatedAt().toLocalDate())
.build();
}

public static MissionResponseDTO.MissionDTO toMissionDTO(UserMission userMission){

return MissionResponseDTO.MissionDTO.builder()
.missionName(userMission.getMission().getName())
.contents(userMission.getMission().getContents())
.points(userMission.getMission().getPoints())
.restaurantId(userMission.getMission().getRestaurant().getId())
.createdAt(userMission.getMission().getCreatedAt().toLocalDate())
.build();
}



public static MissionResponseDTO.MissionListDTO toMissionListDTO(Page<Mission> missionList){

List<MissionResponseDTO.MissionDTO> missionDTOList = missionList.stream()
.map(MissionConverter::toMissionDTO).collect(Collectors.toList());

return MissionResponseDTO.MissionListDTO.builder()
.isLast(missionList.isLast())
.isFirst(missionList.isFirst())
.totalPage(missionList.getTotalPages())
.totalElements(missionList.getTotalElements())
.listSize(missionDTOList.size())
.missionDTOList(missionDTOList)
.build();
}

public static MissionResponseDTO.MissionListDTO toUserMissionListDTO(Page<UserMission> userMissionList) {

List<MissionResponseDTO.MissionDTO> userMissionDTOList = userMissionList.stream()
.map(MissionConverter::toMissionDTO).collect(Collectors.toList());
Comment on lines +72 to +73
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 리스트로 안 받고, Page로 받으면 밑에서 따로 Page 관련 값들 세팅 안 해도 돼서 더 효율적일 것 같아요 !

Page<MissionResponseDTO.MissionDTO> userMissionDTOPage = userMissionList.map(MissionConverter::toMissionDTO);


return MissionResponseDTO.MissionListDTO.builder()
.isLast(userMissionList.isLast())
.isFirst(userMissionList.isFirst())
.totalPage(userMissionList.getTotalPages())
.totalElements(userMissionList.getTotalElements())
.listSize(userMissionDTOList.size())
.missionDTOList(userMissionDTOList)
.build();

}
}
Loading