Skip to content

Commit

Permalink
로그인 세션 관리 기능 구현 (#34)
Browse files Browse the repository at this point in the history
* feat(BE): SecurityFilter 클래스 생성
controller 앞 단에서 http request를 가로채어 인증. 인가 정보를 파악한 뒤 이후 로직으로 실행 분기

* feat(BE): SecurityFilter 클래스 애노테이션 등록
DI를 위하여 @component 애노테이션을 추가하고, 이후의 필터와의 순서를 맞추기 위해 @order 애노테이션을 추가

* chore(BE): doFilter의 반복 호출 코드 변경

chain의 doFilter를 호출해야하는데 SecurityFilter의 doFilter를 호출하게 하여 iterative call가 발생하여 stack overflow문제가 발생하는 문제 해결

* feat(BE): 세션 존재 여부에 따라 달라지는 실행 흐름 SecurityFilterTest 생성
@WebMvcTest를 통하여 mockMvc 객체를 생성하여 세션에 따라 컨트롤러 단에 접근 여부가 결정되는 양상을 테스팅

* feat(BE): 필터에서 유저의 authentication/authorization을 판별하기 위한 MemberDetail 생성
향후 Authentication 객체의 principal로 사용되며 authorizationFilter에서 각각의 path에 맞는 접근 권한 판별 시 사용

* feat(BE): MemberDetail에 대한 서비스 객체 생성
MemberRepository를 주입받아 유저의 id에서 memberDetail을 retrieve 하는 loadMemberByName 메소드를 포함하는 Service 객체

* feat(BE): 유저 인증정보에 관한 조작 메소드를 담는 Authentication 인터페이스 구현
Principal, credentail, role, authenicated 여부에 관한 정보를 가져올 수 있도록 메소드 생성

* feat(BE): 유저 인증 성공 시 발급되는 UsernameAndPasswordToken 구현
MemberDetail을 Principal로 가지고 있으며 향후 구현될 AuthenticateFilter에서 인증 여부를 판별하는 데 사용될 것임

* feat(BE): UsernameAndPasswordToken의 provider 클래스 구현
필터에서 파싱된 Username으로 부터 memberDetail을 가져오고 이를 탕으로 UsernameAndPasswordToken를 생성하는 로직 구현

* feat(BE): request에서 유저의 인증 정보를 파악하기 위한 filter 클래스 생성
컨트롤러가 트리거 되기 이전에 request를 가로채와서 session에 들어있는 유저의 정보를 바탕으로 로그인 여부를 판별하여 이후 흐름으로 실행을 이어감

* feat(BE): ContextHolder 클래스 생성
사용자의 요청 시 Authentication 정보가 담기는 클래스. ThreadLocal로 구성하여 매 요청마다 별도의 context가 생성. static 필드로 구성하여 별도의 인스턴스 생성 필요 없음

* feat(BE): 각 요청의 접근 권한을 정의하는 UrlAuthorizationConfigurer 클래스 생성
향후 httpSecurity를 통해 등록될 url의 경로에 대한 정보가 inner class인 UrlRegisty에 담겨 있고, 내부 메소드인 hasRole, permitAll, isAuthenticated를 통해 각각의 경로에 대한 접근 권한을 정의

* feat(BE): path에 대한 권한 정보를 열거형으로 담는 AccessEnum 생성
권한에 대한 이름과, 해당 권한을 처리하기 위한 validate 메소드를 abstract로 정의함

* feat(BE): request에 대한 security 관련 사항을 설정할 수 있는 HttpSecurity 생성
ExpressionUrlAuthorizationConfigurer를 autowired 받으며 리퀘스트에 대한 접근 권한을 판별할 수 있으며 향후 개발 시 csrf 등과 같은 보안 사항이 생겨날 경우 해당 건에 대한 configurer를 autowired 받을 수 있도록 구성할 예정

* feat(BE): 접근 권한 유효성 판별을 위한 AuthorizationFilter 구현
인증된 사용자에 대하여 해당 요청에 접근할 수 있는지 여부를 판별하기 위한 필터 클래스

* feat(BE): 수동 di를 위한 SecurityConfig 구현
AuthorizationFilter와 HttpSecurity 간 의존성 주입을 위한 config 파일. authenticationFilter 이후에 실행될 수 있도록 order를 2로 설정

* test(BE): Test 주석 처리
공통 작업 처리를 위하여 해당 부분 테스트 주석 처리, 향후 구현 재개
  • Loading branch information
i5046821854 authored Feb 8, 2023
1 parent 78bcfcb commit 5c43b38
Show file tree
Hide file tree
Showing 13 changed files with 459 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.be.dorothy.auth;

import dev.be.dorothy.auth.authentication.Authentication;
import dev.be.dorothy.auth.authentication.UsernameAndPasswordTokenProvider;
import dev.be.dorothy.member.service.MemberResDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Component
@Order(1)
public class AuthenticationFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
private final UsernameAndPasswordTokenProvider usernameAndPasswordTokenProvider;

public AuthenticationFilter(UsernameAndPasswordTokenProvider usernameAndPasswordTokenProvider) {
this.usernameAndPasswordTokenProvider = usernameAndPasswordTokenProvider;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpSession session = req.getSession(false);
Authentication authentication = attemptAuthentication(session);
ContextHolder.setContext(authentication);
chain.doFilter(request, response);
}

private Authentication attemptAuthentication(HttpSession session) {
MemberResDto member = (MemberResDto) session.getAttribute("member");
//TODO : 멤버 정보를 바탕으로 로그인 여부 판별 로직 구현

return usernameAndPasswordTokenProvider.getAuthentication(member.getMemberId());
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
logger.info("filter init");
}

@Override
public void destroy() {
Filter.super.destroy();
}
}
38 changes: 38 additions & 0 deletions backend/src/main/java/dev/be/dorothy/auth/AuthorizationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dev.be.dorothy.auth;

import dev.be.dorothy.auth.authorization.HttpSecurity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class AuthorizationFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
private final HttpSecurity httpSecurity;

public AuthorizationFilter(HttpSecurity httpSecurity) {
this.httpSecurity = httpSecurity;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String url = req.getRequestURI();
httpSecurity.validateRequest((MemberDetail)ContextHolder.getContext().getPrincipal(), url);
chain.doFilter(request, response);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
logger.info("filter init");
}

@Override
public void destroy() {
Filter.super.destroy();
}
}
20 changes: 20 additions & 0 deletions backend/src/main/java/dev/be/dorothy/auth/ContextHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.be.dorothy.auth;

import dev.be.dorothy.auth.authentication.Authentication;

public class ContextHolder {

private static final ThreadLocal<Authentication> context = new ThreadLocal<>();

private ContextHolder() {
}

public static Authentication getContext() {
return context.get();
}

public static void setContext(Authentication authentication) {
context.set(authentication);
}

}
34 changes: 34 additions & 0 deletions backend/src/main/java/dev/be/dorothy/auth/MemberDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dev.be.dorothy.auth;

import dev.be.dorothy.member.Member;
import dev.be.dorothy.member.MemberRole;

public class MemberDetail {

private final String username;
private final String password;
private final MemberRole role;

private MemberDetail(String username, String password, MemberRole role) {
this.username = username;
this.password = password;
this.role = role;
}

public static MemberDetail from(Member member){
return new MemberDetail(member.getMemberId(),
member.getPassword(),
member.getRole());
}
public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

public MemberRole getRole() {
return role;
}
}
21 changes: 21 additions & 0 deletions backend/src/main/java/dev/be/dorothy/auth/MemberDetailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.be.dorothy.auth;

import dev.be.dorothy.exception.InternalServerErrorException;
import dev.be.dorothy.member.Member;
import dev.be.dorothy.member.repository.MemberRepository;
import org.springframework.stereotype.Service;

@Service
public class MemberDetailService {

private final MemberRepository memberRepository;

public MemberDetailService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public MemberDetail loadMemberByName(String userId){
Member member = memberRepository.findByMemberId(userId).orElseThrow(InternalServerErrorException::new);
return MemberDetail.from(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.be.dorothy.auth.authentication;

public interface Authentication {

Object getPrincipal();

Object getCredential();

Object getRole();

boolean isAuthenticated();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.be.dorothy.auth.authentication;

import dev.be.dorothy.auth.MemberDetail;
import dev.be.dorothy.member.MemberRole;

public class UsernameAndPasswordToken implements Authentication {

private final MemberDetail memberDetail;
private final String credential;

private final MemberRole memberRole;
private final boolean isAuthenticated;

public UsernameAndPasswordToken(MemberDetail memberDetail, String credential, MemberRole memberRole, boolean isAuthenticated) {
this.memberDetail = memberDetail;
this.credential = credential;
this.memberRole = memberRole;
this.isAuthenticated = isAuthenticated;
}

@Override
public Object getPrincipal() {
return memberDetail;
}

@Override
public Object getCredential() {
return credential;
}

@Override
public Object getRole(){
return memberRole;
}

@Override
public boolean isAuthenticated() {
return isAuthenticated;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.be.dorothy.auth.authentication;

import dev.be.dorothy.auth.MemberDetail;
import dev.be.dorothy.auth.MemberDetailService;
import org.springframework.stereotype.Component;

@Component
public class UsernameAndPasswordTokenProvider {

private final MemberDetailService memberDetailService;

public UsernameAndPasswordTokenProvider(MemberDetailService memberDetailService) {
this.memberDetailService = memberDetailService;
}

public Authentication getAuthentication(String username){
MemberDetail memberDetail = memberDetailService.loadMemberByName(username);
return new UsernameAndPasswordToken(memberDetail, "", memberDetail.getRole(), true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.be.dorothy.auth.authorization;

import dev.be.dorothy.auth.MemberDetail;
import dev.be.dorothy.exception.BadRequestException;
import dev.be.dorothy.member.MemberRole;
import java.util.List;

public enum AcccessEnum {

PERMIT_ALL{
@Override
public void validate(MemberDetail member, List<MemberRole> roleList){
//TODO : 아무 일도 하지 않은 메소드의 처리
}
},
IS_AUTHENTICATED{
@Override
public void validate(MemberDetail member, List<MemberRole> roleList){
if(member == null)
throw new BadRequestException("사용자 정보가 존재하지 않습니다.");
}},
HAS_ROLE{
@Override
public void validate(MemberDetail member, List<MemberRole> roleList) {
if(!roleList.contains(member.getRole())){
throw new BadRequestException("권한이 존재하지 않습니다");
}
}};

public abstract void validate(MemberDetail member, List<MemberRole> roleList);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package dev.be.dorothy.auth.authorization;
import dev.be.dorothy.member.MemberRole;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class ExpressionUrlAuthorizationConfigurer {

private final List<UrlRegistry> urlRegistryList = new ArrayList<>();

private HttpSecurity httpSecurity;

public void setHttpSecurity(HttpSecurity httpSecurity) {
this.httpSecurity = httpSecurity;
}

public UrlRegistry antMatchers(String path){
UrlRegistry registry = urlRegistryList.stream()
.filter(urlRegistry -> urlRegistry.getPath().equals(path))
.findFirst().orElse(new UrlRegistry(path));
urlRegistryList.add(registry);
return registry;
}

public HttpSecurity and(){
return this.httpSecurity;
}

class UrlRegistry {

private final String path;

private final List<AcccessEnum> accessList;
private final List<MemberRole> roleList;

public UrlRegistry(String path) {
this.path = path;
accessList = new ArrayList<>();
roleList = new ArrayList<>();
}

public void addAccessList(AcccessEnum access){
accessList.add(access);
}

public String getPath() {
return path;
}
public List<AcccessEnum> getAccessList() {
return accessList;
}
public List<MemberRole> getRoleList() {
return roleList;
}

public ExpressionUrlAuthorizationConfigurer permitAll(){
addAccessList(AcccessEnum.PERMIT_ALL);
return ExpressionUrlAuthorizationConfigurer.this;
}

public ExpressionUrlAuthorizationConfigurer hasRole(MemberRole memberRole){
addAccessList(AcccessEnum.HAS_ROLE);
roleList.add(memberRole);
return ExpressionUrlAuthorizationConfigurer.this;
}

public ExpressionUrlAuthorizationConfigurer isAuthenticated(){
addAccessList(AcccessEnum.IS_AUTHENTICATED);
return ExpressionUrlAuthorizationConfigurer.this;
}

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dev.be.dorothy.auth.authorization;

import dev.be.dorothy.auth.AuthorizationFilter;
import dev.be.dorothy.auth.MemberDetail;
import dev.be.dorothy.member.MemberRole;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class HttpSecurity {

private final ExpressionUrlAuthorizationConfigurer expressionUrlAuthorizationConfigurer;

public HttpSecurity(ExpressionUrlAuthorizationConfigurer expressionUrlAuthorizationConfigurer) {
this.expressionUrlAuthorizationConfigurer = expressionUrlAuthorizationConfigurer;
expressionUrlAuthorizationConfigurer.setHttpSecurity(this);
}

public ExpressionUrlAuthorizationConfigurer authorizeRequest(){
return expressionUrlAuthorizationConfigurer;
}

public void validateRequest(MemberDetail member, String url){
ExpressionUrlAuthorizationConfigurer.UrlRegistry urlRegistry = expressionUrlAuthorizationConfigurer.antMatchers(url);
List<AcccessEnum> accessList = urlRegistry.getAccessList();
List<MemberRole> roleList = urlRegistry.getRoleList();
accessList.forEach(acccessEnum -> acccessEnum.validate(member, roleList));
}

public AuthorizationFilter build() {
return new AuthorizationFilter(HttpSecurity.this);
}
}
Loading

0 comments on commit 5c43b38

Please sign in to comment.