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

2단계 - 리팩터링 #7

Open
wants to merge 2 commits into
base: kang-hyungu
Choose a base branch
from
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
21 changes: 17 additions & 4 deletions src/main/java/nextstep/app/config/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
package nextstep.app.config;

import nextstep.security.access.matcher.AnyRequestMatcher;
import nextstep.security.access.matcher.MvcRequestMatcher;
import nextstep.security.authentication.AuthenticationManager;
import nextstep.security.authentication.BasicAuthenticationFilter;
import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
import nextstep.security.authentication.UsernamePasswordAuthenticationProvider;
import nextstep.security.authorization.AuthorizationFilter;
import nextstep.security.config.AuthorizeRequestMatcherRegistry;
import nextstep.security.config.DefaultSecurityFilterChain;
import nextstep.security.config.FilterChainProxy;
import nextstep.security.config.SecurityFilterChain;
import nextstep.security.context.HttpSessionSecurityContextRepository;
import nextstep.security.context.SecurityContextHolderFilter;
import nextstep.security.context.SecurityContextRepository;
import nextstep.security.userdetails.UserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.List;

@Configuration
Expand All @@ -41,9 +45,12 @@ public FilterChainProxy filterChainProxy() {

@Bean
public SecurityFilterChain securityFilterChain() {
List<Filter> filters = new ArrayList<>();
filters.add(new UsernamePasswordAuthenticationFilter(authenticationManager(), securityContextRepository()));
filters.add(new BasicAuthenticationFilter(authenticationManager()));
List<Filter> filters = List.of(
new SecurityContextHolderFilter(securityContextRepository()),
new UsernamePasswordAuthenticationFilter(authenticationManager(), securityContextRepository()),
new BasicAuthenticationFilter(authenticationManager()),
new AuthorizationFilter()
);
return new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, filters);
}

Expand All @@ -57,4 +64,10 @@ public AuthenticationManager authenticationManager() {
return new AuthenticationManager(new UsernamePasswordAuthenticationProvider(userDetailsService));
}

@Bean
public AuthorizeRequestMatcherRegistry authorizeRequestMatcherRegistry() {
return new AuthorizeRequestMatcherRegistry()
.matcher(new MvcRequestMatcher(HttpMethod.GET, "/members")).hasAuthority("ADMIN")
.matcher(new MvcRequestMatcher(HttpMethod.GET, "/members/me")).authenticated();
}
}
2 changes: 1 addition & 1 deletion src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

@RestController
public class LoginController {

@PostMapping("/login")
public ResponseEntity<Void> login() {
return ResponseEntity.ok().build();
}

}
29 changes: 15 additions & 14 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,36 @@

import nextstep.app.domain.MemberRepository;
import nextstep.app.ui.dto.MemberDto;
import nextstep.security.context.SecurityContextHolder;
import nextstep.security.exception.AuthenticationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class MemberController {

private final MemberRepository memberRepository;

public MemberController(MemberRepository memberRepository) {
public MemberController(final MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@GetMapping("/members")
public ResponseEntity<List<MemberDto>> list() {
List<MemberDto> members = memberRepository.findAll()
.stream()
.map(member -> new MemberDto(
member.getEmail(),
member.getPassword(),
member.getName(),
member.getImageUrl(),
member.getRoles())
)
.collect(Collectors.toList());
return ResponseEntity.ok(members);
final var members = memberRepository.findAll();
return ResponseEntity.ok(MemberDto.toList(members));
}

@GetMapping("/members/me")
public ResponseEntity<MemberDto> me() {
final var authentication = SecurityContextHolder.getContext().getAuthentication();
final var email = (String) authentication.getPrincipal();

final var member = memberRepository.findByEmail(email)
.orElseThrow(AuthenticationException::new);

return ResponseEntity.ok(MemberDto.toDto(member));
}
}
27 changes: 23 additions & 4 deletions src/main/java/nextstep/app/ui/dto/MemberDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import nextstep.app.domain.Member;

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

public class MemberDto {

Expand All @@ -15,17 +18,33 @@ public class MemberDto {

@JsonCreator
public MemberDto(@JsonProperty("email") final String email,
@JsonProperty("password")final String password,
@JsonProperty("name")final String name,
@JsonProperty("imageUrl")final String imageUrl,
@JsonProperty("roles")final Set<String> roles) {
@JsonProperty("password") final String password,
@JsonProperty("name") final String name,
@JsonProperty("imageUrl") final String imageUrl,
@JsonProperty("roles") final Set<String> roles) {
this.email = email;
this.password = password;
this.name = name;
this.imageUrl = imageUrl;
this.roles = roles;
}

public static MemberDto toDto(final Member member) {
return new MemberDto(
member.getEmail(),
member.getPassword(),
member.getName(),
member.getImageUrl(),
member.getRoles()
);
}

public static List<MemberDto> toList(final List<Member> members) {
return members.stream()
.map(MemberDto::toDto)
.collect(Collectors.toList());
}

public String getEmail() {
return email;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface Authentication {
Set<String> getAuthorities();

boolean isAuthenticated();

boolean isAdmin();
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 메서드가 프레임워크의 인터페이스까지 들어와야하나 조금 고민이 되네요.
모든 애플리케이션의 인증 객체에 admin이란 권한이 들어가나 싶어서요 ㅎㅎ
구구는 어떻게 생각하시나요?

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import nextstep.security.context.SecurityContextHolder;
import nextstep.security.exception.AuthenticationException;
import nextstep.security.exception.AuthorizationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.GenericFilterBean;

Expand All @@ -21,34 +20,27 @@ public class BasicAuthenticationFilter extends GenericFilterBean {

private final AuthenticationManager authenticationManager;

public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
public BasicAuthenticationFilter(final AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

try {
Authentication authRequest = convertAuthenticationRequest((HttpServletRequest) request);
final var authRequest = convertAuthenticationRequest((HttpServletRequest) request);

if (authRequest == null) {
chain.doFilter(request, response);
return;
}

Authentication authResult = authenticationManager.authenticate(authRequest);
final var authResult = authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);

if (authResult.getAuthorities().isEmpty()) {
throw new AuthorizationException();
}
} catch (AuthenticationException e) {
SecurityContextHolder.clearContext();
((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
} catch (AuthorizationException e) {
((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}

chain.doFilter(request, response);
Expand All @@ -75,6 +67,4 @@ private Authentication convertAuthenticationRequest(HttpServletRequest request)

return UsernamePasswordAuthentication.ofRequest(email, password);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ public boolean isAuthenticated() {
return authenticated;
}

public boolean isAdmin() {
return getAuthorities()
.contains("ADMIN");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package nextstep.security.authentication;

import nextstep.security.access.matcher.MvcRequestMatcher;
import nextstep.security.context.SecurityContext;
import nextstep.security.context.SecurityContextHolder;
import nextstep.security.context.SecurityContextRepository;
import nextstep.security.exception.AuthenticationException;
import nextstep.security.exception.AuthorizationException;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.GenericFilterBean;
Expand All @@ -20,48 +18,44 @@

public class UsernamePasswordAuthenticationFilter extends GenericFilterBean {

private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST, "/login");

private final AuthenticationManager authenticationManager;
private final SecurityContextRepository securityContextRepository;
private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST,
"/login");

public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager, SecurityContextRepository securityContextRepository) {
public UsernamePasswordAuthenticationFilter(final AuthenticationManager authenticationManager,
final SecurityContextRepository securityContextRepository) {
this.authenticationManager = authenticationManager;
this.securityContextRepository = securityContextRepository;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {

final var request = (HttpServletRequest) servletRequest;

try {
if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) request)) {
if (!DEFAULT_REQUEST_MATCHER.matches(request)) {
chain.doFilter(request, response);
return;
}

String username = request.getParameter("username");
String password = request.getParameter("password");

UsernamePasswordAuthentication authRequest = UsernamePasswordAuthentication.ofRequest(username,
final var authRequest = UsernamePasswordAuthentication.ofRequest(username,
password);
Authentication authResult = authenticationManager.authenticate(authRequest);
final var authResult = authenticationManager.authenticate(authRequest);

SecurityContext context = SecurityContextHolder.createEmptyContext();
final var context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);

securityContextRepository.saveContext(context, (HttpServletRequest) request, (HttpServletResponse) response);

if (authResult.getAuthorities().isEmpty()) {
throw new AuthorizationException();
}
securityContextRepository.saveContext(context, request, (HttpServletResponse) response);
} catch (AuthenticationException e) {
SecurityContextHolder.clearContext();
((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
} catch (AuthorizationException e) {
((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}

chain.doFilter(request, response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nextstep.security.authorization;

import nextstep.security.context.SecurityContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthorizationFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이번 요구사항에서 추가된 GET /members/me 요청은 사용자가 인증이 완료 되었으면 접근 가능합니다.
즉, 모든 Member는 권한이 없더라도 자신의 정보를 볼 수 있죠.

먼저 AuthorizationFilter에 인증이 완료된 사용자일 때 403 에러를 받지 않도록 로직을 추가한 후, 리팩터링을 해보면 좋을 것 같네요.
리팩터링 과정에서 요청에 맞는 인가 방법을 확인할 때, AuthConfig에서 설정한 AuthorizeRequestMatcherRegistry 를 참고하시면 되겠습니다!

final var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
sendError(response, HttpStatus.UNAUTHORIZED);
return;
}

if (!authentication.isAdmin()) {
sendError(response, HttpStatus.FORBIDDEN);
return;
}

chain.doFilter(request, response);
}

private static void sendError(final ServletResponse response, final HttpStatus httpStatus) throws IOException {
((HttpServletResponse) response).sendError(httpStatus.value(), httpStatus.getReasonPhrase());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nextstep.security.context;

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

public class SecurityContextHolderFilter extends OncePerRequestFilter {

private final SecurityContextRepository securityContextRepository;

public SecurityContextHolderFilter(final SecurityContextRepository securityContextRepository) {
this.securityContextRepository = securityContextRepository;
}

@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
Optional.ofNullable(securityContextRepository.loadContext(request))
.ifPresent(SecurityContextHolder::setContext);

filterChain.doFilter(request, response);
}
}
Loading