Skip to content

Commit

Permalink
Merge pull request #206 from SMUMC-6th/feat/#204
Browse files Browse the repository at this point in the history
✨ feat#204:  jwt 인증 인가 구현
  • Loading branch information
kimdavid0521 authored Jun 30, 2024
2 parents 281f515 + b10f99c commit ba95766
Show file tree
Hide file tree
Showing 12 changed files with 529 additions and 45 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ public enum ErrorStatus implements BaseErrorCode {
//post에러
_NOT_FOUND_POST(HttpStatus.NOT_FOUND, "POST400", "게시글을 찾을 수 없습니다"),

//reply에러
_NOT_FOUND_REPLY(HttpStatus.NOT_FOUND, "REPLY400", "댓글을 찾을 수 없습니다"),

//테스트 에러
_TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이것은 테스트");
_TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이것은 테스트"),

// Reply 에러
_NOT_FOUND_REPLY(HttpStatus.NOT_FOUND, "REPLY400", "reply를 찾을 수 없습니다."),

// auth 관련 에러
_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH400", "토큰을 찾을 수 없습니다."),
_AUTH_EXPIRE_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH401", "토큰이 만료되었습니다."),
_AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH402", "토큰이 유효하지 않습니다."),

_AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "AUTH403", "인증 실패");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.umc.study.apiPayload.exception.handler;

import com.example.umc.study.apiPayload.code.BaseErrorCode;
import com.example.umc.study.apiPayload.exception.GeneralException;

public class AuthHandler extends GeneralException {
public AuthHandler(BaseErrorCode code) {
super(code);
}
}
179 changes: 138 additions & 41 deletions src/main/java/com/example/umc/study/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,78 +1,175 @@
package com.example.umc.study.config;

//import com.example.umc.study.config.filter.TestFilter;
//import com.example.umc.study.config.filter.TestFilter2;
//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.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.www.BasicAuthenticationFilter;
//
//@Configuration
//@EnableWebSecurity(debug = true)
//public class SecurityConfig {
//
// private final String[] allowUrl = {
// "/swagger-ui/**",
// "/swagger-resources/**",
// "/v3/api-docs/**",
// "/api/v1/posts/**",
// "/api/v1/replies/**",
// "/test"
// };
//
//
//// @Bean
//// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
////
//// http.authorizeHttpRequests((requests) -> requests
//// .requestMatchers("/swagger-ui/**").permitAll()
//// .anyRequest().authenticated());
//// return http.build();
//// }
//
// @Bean
// SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// http.csrf(AbstractHttpConfigurer::disable);
// http.authorizeHttpRequests((requests) -> requests
//
//// .requestMatchers("/api/v1/replies/**").permitAll() //api/v1/replies/**은 /api/v1/replies가 앞으로 붙어있는 모든 주소에 대해 허용
//// .requestMatchers("api/v1/users/**").hasAnyAuthority("admin")
//// .requestMatchers("/api/v1/posts/**").permitAll()//api/v1/users/**은 /api/v1/users가 앞으로 붙어있는 모든 주소에 대해 admin 역할을 가진 user에게 허용
// .requestMatchers(HttpMethod.POST,"/api/v1/users").permitAll()
//// .requestMatchers(HttpMethod.POST,"/api/v1/users/{userId}/posts").hasAnyRole("USER", "ADMIN")
//// .requestMatchers(HttpMethod.POST,"/api/v1/replies").hasRole("ADMIN")
// .requestMatchers(HttpMethod.POST, "/api/v1/users/{userId}/posts").hasAnyRole("ADMIN", "USER")
// .requestMatchers(HttpMethod.PATCH, "/api/v1/posts/{postId}").hasAnyRole("ADMIN","USER")
// .requestMatchers(HttpMethod.POST, "/users/{userId}/posts/{postId}/replies/").hasRole("ADMIN")
// .requestMatchers(allowUrl).permitAll()
// .anyRequest().authenticated()); // anyRequest.authenticated는 나머지 모든 request에 대해 인증
// http.formLogin(AbstractHttpConfigurer::disable);
// http.httpBasic(AbstractHttpConfigurer::disable);
// http.
// sessionManagement((session) -> session
// .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// //http.addFilterAfter(new TestFilter(), BasicAuthenticationFilter.class);
// http.addFilterAfter(new TestFilter2(), BasicAuthenticationFilter.class);
// return http.build();
// }
//
// @Bean
// public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// }
//
//// @Bean
//// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//// http.csrf(AbstractHttpConfigurer::disable);
//// http.authorizeHttpRequests((requests) -> requests
//// .anyRequest().authenticated()
//// );
//// return http.build();
//// }
//
//
//}


import com.example.umc.study.config.filter.TestFilter;
import com.example.umc.study.config.filter.TestFilter2;
import com.example.umc.study.config.filter.*;
import com.example.umc.study.config.jwt.JWTUtil;
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.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
public class SecurityConfig {

private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final PrincipalDetailService principalDetailsService;
private final JWTAccessDeniedHandler jwtAccessDeniedHandler;
private final JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;

private final String[] allowUrl = {
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/api/v1/posts/**",
"/api/v1/replies/**",
"/test"
"api/v1/replies/**"
};


// @Bean
// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//
// http.authorizeHttpRequests((requests) -> requests
// .requestMatchers("/swagger-ui/**").permitAll()
// .anyRequest().authenticated());
// return http.build();
// }

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests((requests) -> requests

// .requestMatchers("/api/v1/replies/**").permitAll() //api/v1/replies/**은 /api/v1/replies가 앞으로 붙어있는 모든 주소에 대해 허용
// .requestMatchers("api/v1/users/**").hasAnyAuthority("admin")
// .requestMatchers("/api/v1/posts/**").permitAll()//api/v1/users/**은 /api/v1/users가 앞으로 붙어있는 모든 주소에 대해 admin 역할을 가진 user에게 허용
.requestMatchers(HttpMethod.POST,"/api/v1/users").permitAll()
// .requestMatchers(HttpMethod.POST,"/api/v1/users/{userId}/posts").hasAnyRole("USER", "ADMIN")
// .requestMatchers(HttpMethod.POST,"/api/v1/replies").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/users/{userId}/posts").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.PATCH, "/api/v1/posts/{postId}").hasAnyRole("ADMIN","USER")
.requestMatchers(HttpMethod.POST, "/users/{userId}/posts/{postId}/replies/").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users/{userId}/posts").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.PATCH, "/api/v1//posts/{postId}").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/users/{userId}/posts/{postId}/replies").hasAnyRole("ADMIN")
.requestMatchers(HttpMethod.PATCH, "/api/v1/replies/{replyId}").hasAnyRole("ADMIN")
.requestMatchers(allowUrl).permitAll()
.anyRequest().authenticated()); // anyRequest.authenticated는 나머지 모든 request에 대해 인증
http.formLogin(Customizer.withDefaults());
http.httpBasic(Customizer.withDefaults());
//http.addFilterAfter(new TestFilter(), BasicAuthenticationFilter.class);
http.addFilterAfter(new TestFilter2(), BasicAuthenticationFilter.class);
.anyRequest().authenticated());
/*http.addFilterAfter(new TestFilter(), BasicAuthenticationFilter.class);*/
http.addFilterAfter(new TestFilter2(), AnonymousAuthenticationFilter.class);
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
/*http.addFilterAfter(new TestFilter2(), AnonymousAuthenticationFilter.class);*/
http.exceptionHandling(
(configurer ->
configurer
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)
);

http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil),
UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JWTFilter(jwtUtil, principalDetailsService),
UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JWTExceptionFilter(), JWTFilter.class);
// form 로그인 방식 disable
http.formLogin(AbstractHttpConfigurer::disable);
// http basic 인증 방식 disable
http.httpBasic(AbstractHttpConfigurer::disable);
// Session Stateless하게 관리
http.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// @Bean
// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http.csrf(AbstractHttpConfigurer::disable);
// http.authorizeHttpRequests((requests) -> requests
// .anyRequest().authenticated()
// );
// return http.build();
// }


}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.umc.study.config.filter;


import com.example.umc.study.apiPayload.BaseResponse;
import com.example.umc.study.apiPayload.code.status.ErrorStatus;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JWTAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(403);

BaseResponse<Object> errorResponse =
BaseResponse.onFailure(
ErrorStatus._FORBIDDEN.getCode(),
ErrorStatus._FORBIDDEN.getMessage(),
null);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.umc.study.config.filter;


import com.example.umc.study.apiPayload.BaseResponse;
import com.example.umc.study.apiPayload.code.status.ErrorStatus;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(401);

BaseResponse<Object> errorResponse =
BaseResponse.onFailure(
ErrorStatus._UNAUTHORIZED.getCode(),
ErrorStatus._UNAUTHORIZED.getMessage(),
null);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.umc.study.config.filter;

import com.example.umc.study.apiPayload.BaseResponse;
import com.example.umc.study.apiPayload.code.status.ErrorStatus;
import com.example.umc.study.apiPayload.exception.handler.AuthHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JWTExceptionFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (AuthHandler e) {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(e.getErrorReasonHttpStatus().getHttpStatus().value());

ErrorStatus code = (ErrorStatus) e.getCode();

BaseResponse<Object> errorResponse =
BaseResponse.onFailure(code.getCode(), code.getMessage(), null);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), errorResponse);
}

}
}
Loading

0 comments on commit ba95766

Please sign in to comment.