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 spring/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
Expand All @@ -40,6 +42,16 @@ dependencies {

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

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

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions spring/src/main/generated/umc/spring/domain/QStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class QStore extends EntityPathBase<Store> {

public final QRegion region;

public final NumberPath<Float> score = createNumber("score", Float.class);

//inherited
public final DateTimePath<java.time.LocalDateTime> updatedAt = _super.updatedAt;

Expand Down
8 changes: 8 additions & 0 deletions spring/src/main/generated/umc/spring/domain/QUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,24 @@ public class QUser extends EntityPathBase<User> {
//inherited
public final DateTimePath<java.time.LocalDateTime> createdAt = _super.createdAt;

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

public final EnumPath<umc.spring.domain.enums.Gender> gender = createEnum("gender", umc.spring.domain.enums.Gender.class);

public final NumberPath<Long> id = createNumber("id", Long.class);

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

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

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

public final ListPath<FoodCategory, QFoodCategory> preferCategory = this.<FoodCategory, QFoodCategory>createList("preferCategory", FoodCategory.class, QFoodCategory.class, PathInits.DIRECT2);

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

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

//inherited
public final DateTimePath<java.time.LocalDateTime> updatedAt = _super.updatedAt;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.spring.apiPayload.exception.handler;


import lombok.Getter;
import umc.spring.apiPayload.status.ErrorResponse;

@Getter
public class UserHandler extends RuntimeException {

private final ErrorResponse errorResponse;

public UserHandler(ErrorResponse errorResponse) {
super(errorResponse.getMessage());
this.errorResponse = errorResponse;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ public enum ErrorResponse implements BaseErrorResponse {

// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"),
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),

// jwt 에러
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401","유효하지 않은 토큰입니다."),
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "JWT402", "비밀번호가 일치하지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package umc.spring.config.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
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.spring.config.properties.Constants;

@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 spring/src/main/java/umc/spring/config/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package umc.spring.config.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 java.security.Key;
import java.util.Date;
import java.util.Collections;
import umc.spring.apiPayload.exception.handler.UserHandler;
import umc.spring.apiPayload.status.ErrorResponse;
import umc.spring.config.properties.Constants;
import umc.spring.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 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(ErrorResponse.INVALID_TOKEN);
}
return getAuthentication(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package umc.spring.config.properties;

public final class Constants {
public static final String AUTH_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package umc.spring.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,28 @@
package umc.spring.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.spring.domain.User;
import umc.spring.repository.userrepository.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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package umc.spring.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.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.spring.config.jwt.JwtAuthenticationFilter;
import umc.spring.config.jwt.JwtTokenProvider;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

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

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
System.out.println("hi");
return new BCryptPasswordEncoder();
}
}
33 changes: 33 additions & 0 deletions spring/src/main/java/umc/spring/config/swagger/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package umc.spring.config.swagger;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
final String securitySchemeName = "JWT TOKEN"; // @SecurityRequirement 이름과 일치해야 함

return new OpenAPI()
.info(new Info()
.title("U.M.C API 문서")
.description("JWT 인증 테스트용 Swagger 문서입니다.")
.version("v1.0"))
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) // 인증 적용
.components(new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
));
}
}
Loading