Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : Spring Security 설정 #7

Merged
merged 15 commits into from
Feb 2, 2025
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
application-jwt.yml

### STS ###
.apt_generated
Expand Down
16 changes: 14 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'

annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// jwts
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

// gson
implementation 'com.google.code.gson:gson'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package team.eusha.lifewise.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import team.eusha.lifewise.security.jwt.filter.JwtAuthenticationFilter;
import team.eusha.lifewise.security.jwt.provider.JwtAuthenticationProvider;

@Configuration
@RequiredArgsConstructor
public class AuthenticationManagerConfig extends AbstractHttpConfigurer<AuthenticationManagerConfig, HttpSecurity> {

private final JwtAuthenticationProvider jwtAuthenticationProvider;

@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

builder.addFilterBefore(
new JwtAuthenticationFilter(authenticationManager),
UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(jwtAuthenticationProvider);
}



}
49 changes: 49 additions & 0 deletions src/main/java/team/eusha/lifewise/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package team.eusha.lifewise.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.*;
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 team.eusha.lifewise.security.jwt.exception.CustomAuthenticationEntryPoint;

// Spring Security 설정
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final AuthenticationManagerConfig authenticationManagerConfig;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(FormLoginConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.cors(Customizer.withDefaults())
.with(authenticationManagerConfig, customizer -> {})
.httpBasic(HttpBasicConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("auth/signup", "/auth/login", "members/refresh").permitAll()
.requestMatchers(HttpMethod.GET, "/**").hasAnyRole("USER", "MANAGER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/**").hasAnyRole("USER", "MANAGER", "ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHanding -> exceptionHanding
.authenticationEntryPoint(customAuthenticationEntryPoint))
.build();

}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
18 changes: 18 additions & 0 deletions src/main/java/team/eusha/lifewise/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package team.eusha.lifewise.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// Spring MVC 에 대한 설정파일. 웹에 대한 설정파일
@Configuration
public class WebConfig implements WebMvcConfigurer {

// CORS
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5137")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package team.eusha.lifewise.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import team.eusha.lifewise.dto.request.MemberLoginRequest;
import team.eusha.lifewise.dto.response.MemberLoginResponse;
import team.eusha.lifewise.security.jwt.util.JwtTokenizer;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

private final JwtTokenizer jwtTokenizer;

@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid MemberLoginRequest login) {

Long memberId = 1L;
String email = login.getEmail();
List<String> roles = List.of("ROLE_USER");

// JWT 토큰 생성
String accessToken = jwtTokenizer.createAccessToken(memberId, email, roles);
String refreshToken = jwtTokenizer.createRefreshToken(memberId, email, roles);

MemberLoginResponse loginResponse = MemberLoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.memberId(memberId)
.name("name")
.build();
return new ResponseEntity(loginResponse, HttpStatus.OK);
}

@DeleteMapping("/logout")
public ResponseEntity logout(@RequestHeader("Authorization") String token) {
// token repository에서 refresh Token에 해당하는 값을 삭제
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package team.eusha.lifewise.dto.request;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginRequest {

@NotEmpty
@Pattern(regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$")
private String email;

@NotEmpty
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*\\W).{8,20}$") // 영문, 특수문자 8자 이상 20자 이하
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package team.eusha.lifewise.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginResponse {
private String accessToken;
private String refreshToken;

private Long memberId;
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package team.eusha.lifewise.security.jwt.exception;

import com.google.gson.Gson;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;

@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exception = (String) request.getAttribute("exception");
log.error("Commence Get Exception : {}", exception);

if(exception == null) {
log.error("entry point >> exception is null");
setResponse(response, JwtExceptionCode.NOT_FOUND_TOKEN);
}
//잘못된 토큰인 경우
else if(exception.equals(JwtExceptionCode.INVALID_TOKEN.getCode())) {
log.error("entry point >> invalid token");
setResponse(response, JwtExceptionCode.INVALID_TOKEN);
}
//토큰 만료된 경우
else if(exception.equals(JwtExceptionCode.EXPIRED_TOKEN.getCode())) {
log.error("entry point >> expired token");
setResponse(response, JwtExceptionCode.EXPIRED_TOKEN);
}
//지원되지 않는 토큰인 경우
else if(exception.equals(JwtExceptionCode.UNSUPPORTED_TOKEN.getCode())) {
log.error("entry point >> unsupported token");
setResponse(response, JwtExceptionCode.UNSUPPORTED_TOKEN);
}
else if (exception.equals(JwtExceptionCode.NOT_FOUND_TOKEN.getCode())) {
log.error("entry point >> not found token");
setResponse(response, JwtExceptionCode.NOT_FOUND_TOKEN);
}
else {
setResponse(response, JwtExceptionCode.UNKNOWN_ERROR);
}

}

private void setResponse(HttpServletResponse response, JwtExceptionCode exceptionCode) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

HashMap<String, Object> errorInfo = new HashMap<>();
errorInfo.put("message", exceptionCode.getMessage());
errorInfo.put("code", exceptionCode.getCode());
Gson gson = new Gson();
String responseJson = gson.toJson(errorInfo);
response.getWriter().print(responseJson);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package team.eusha.lifewise.security.jwt.exception;

import lombok.Getter;

@Getter
public enum JwtExceptionCode {

UNKNOWN_ERROR("UNKNOWN_ERROR", "UNKNOWN_ERROR"),
NOT_FOUND_TOKEN("NOT_FOUND_TOKEN", "Headers에 토큰 형식의 값 찾을 수 없음"),
INVALID_TOKEN("INVALID_TOKEN", "유효하지 않은 토큰"),
EXPIRED_TOKEN("EXPIRED_TOKEN", "기간이 만료된 토큰"),
UNSUPPORTED_TOKEN("UNSUPPORTED_TOKEN", "지원하지 않는 토큰");


private String code;

private String message;

JwtExceptionCode(String code, String message) {
this.code = code;
this.message = message;
}
}
Loading