Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
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.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import static com.example.solidconnection.type.Role.ADMIN;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
Expand Down Expand Up @@ -55,10 +58,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.formLogin(AbstractHttpConfigurer::disable)
.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole(ADMIN.name())
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class)
.addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class)
.addFilterAfter(exceptionHandlerFilter, ExceptionTranslationFilter.class)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public enum ErrorCode {
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."),
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."),
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."),
ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."),

// s3
S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.example.solidconnection.custom.security.authentication;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collections;

public abstract class JwtAuthentication extends AbstractAuthenticationToken {

Expand All @@ -9,7 +12,9 @@ public abstract class JwtAuthentication extends AbstractAuthenticationToken {
private final Object principal;

public JwtAuthentication(String token, Object principal) {
super(null);
super(principal instanceof UserDetails ?
((UserDetails) principal).getAuthorities() :
Collections.emptyList());
this.credentials = token;
this.principal = principal;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.solidconnection.custom.security.filter;

import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.exception.ErrorCode;
import com.example.solidconnection.custom.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
Expand All @@ -9,12 +10,16 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED;
import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED;

@Component
Expand All @@ -31,25 +36,28 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
filterChain.doFilter(request, response);
} catch (CustomException e) {
customCommence(response, e);
} catch (AccessDeniedException e) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
ErrorCode errorCode = auth instanceof AnonymousAuthenticationToken ? AUTHENTICATION_FAILED : ACCESS_DENIED;
generalCommence(response, e, errorCode);
} catch (Exception e) {
generalCommence(response, e);
generalCommence(response, e, AUTHENTICATION_FAILED);
}
}

public void customCommence(HttpServletResponse response, CustomException customException) throws IOException {
SecurityContextHolder.clearContext();
ErrorResponse errorResponse = new ErrorResponse(customException);
writeResponse(response, errorResponse);
writeResponse(response, errorResponse, customException.getCode());
}

public void generalCommence(HttpServletResponse response, Exception exception) throws IOException {
SecurityContextHolder.clearContext();
ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage());
writeResponse(response, errorResponse);
public void generalCommence(HttpServletResponse response, Exception exception, ErrorCode errorCode) throws IOException {
ErrorResponse errorResponse = new ErrorResponse(errorCode, exception.getMessage());
writeResponse(response, errorResponse, errorCode.getCode());
Comment on lines 48 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

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

다시 보니 customCommence(), generalCommence() 이 함수들 접근제한자도 private 이 되는게 좋겠네요🥹

}

private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse, int statusCode) throws IOException {
SecurityContextHolder.clearContext();
response.setStatus(statusCode);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.solidconnection.custom.security.userdetails;

import com.example.solidconnection.type.Role;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.List;

public class SecurityRoleMapper {

private static final String ROLE_PREFIX = "ROLE_";

private SecurityRoleMapper() {
}

public static List<SimpleGrantedAuthority> mapRoleToAuthorities(Role role) {
return List.of(new SimpleGrantedAuthority(ROLE_PREFIX + role.name()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public String getUsername() {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
return SecurityRoleMapper.mapRoleToAuthorities(siteUser.getRole());
}

@Override
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/example/solidconnection/type/Role.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.example.solidconnection.type;

public enum Role {

ADMIN,
MENTOR,
MENTEE
MENTEE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE site_user
modify ROLE enum ('MENTEE', 'MENTOR', 'ADMIN') NOT NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.stream.Stream;
Expand Down Expand Up @@ -82,10 +85,46 @@ void setUp() {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}

@Test
void 익명_사용자의_접근_거부시_401_예외_응답을_반환한다() throws Exception {
// given
Authentication anonymousAuth = getAnonymousAuth();
SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response);

// when
exceptionHandlerFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}

@Test
void 인증된_사용자의_접근_거부하면_403_예외_응답을_반환한다() throws Exception {
// given
Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_USER");
SecurityContextHolder.getContext().setAuthentication(auth);
willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response);

// when
exceptionHandlerFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
}

private static Stream<Throwable> provideException() {
return Stream.of(
new RuntimeException(),
new CustomException(ErrorCode.INVALID_TOKEN)
);
}

private Authentication getAnonymousAuth() {
return new AnonymousAuthenticationToken(
"key",
"anonymousUser",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertAll;

@DisplayName("사용자 인증 정보 서비스 테스트")
@TestContainerSpringBootTest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.solidconnection.custom.security.userdetails;

import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.example.solidconnection.type.Gender;
import com.example.solidconnection.type.PreparationStatus;
import com.example.solidconnection.type.Role;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("사용자 인증 정보 테스트")
@TestContainerSpringBootTest
class SiteUserDetailsTest {

@Autowired
private SiteUserRepository siteUserRepository;

@Test
void 사용자_권한을_정상적으로_반환한다() {
// given
SiteUser siteUser = siteUserRepository.save(createSiteUser());
SiteUserDetails siteUserDetails = new SiteUserDetails(siteUser);

// when
Collection<? extends GrantedAuthority> authorities = siteUserDetails.getAuthorities();

// then
assertThat(authorities)
.extracting("authority")
.containsExactly("ROLE_" + siteUser.getRole().name());
}

private SiteUser createSiteUser() {
return new SiteUser(
"[email protected]",
"nickname",
"profileImageUrl",
"1999-01-01",
PreparationStatus.CONSIDERING,
Role.MENTEE,
Gender.MALE
);
}
}