SpringSecurity를 사용한 인증/인가 프로세스(2) 전체 프로세스

불바다·2023년 8월 28일

KpopGeneration

목록 보기
3/10

아직 SpringSecurity의 세부사항들을 다 알진 못하지만, 기본적으로 이해한 SpringSecurity의 핵심 축은 인증과 인가 프로세스였다

인증을 위한 절차들이 존재하고, 이러한 인증 절차를 거친 후 인가 절차에 들어가게 된다.
인증 절차
해당 사용자가 인증된 사용자인지, 즉 정당하게 로그인한 유저인지 아닌지를 판단하는 절차
인가 절차
이 사용자가 해당 자원에 접근할 수 있는 권한을 가지고 있는지 아닌지 판단하는 절차이다.

인증 절차

다음은 직접 구현한 인증 절차에 대한 간단한 프로세스이다

UsernamePasswordAuthenticationFilter

SpringSercurity가 기본적으로 제공하는 필터 중에 UsernamePasswordAuthenticationFilter가 존재한다. 해당 필터는 username과 password를 가지고 인증 절차에 착수한다. 해당 필터는 username과 password를 가지고 '아직 인증되지 않은' AuthenticationToken을 만든 후 이 토큰을 AuthenticationManger에게 넘겨준다.

AuthenticticationManager

AuthenticationManager는 실제로 인증 과정을 수행할 수 있는 여러 종류의 Provider을 가지고 있다. 이 클래스가 하는 일은 현재 인증 과정을 수행할 수 있는 적절한 Provider를 찾아 인증 요청 정보를 넘김으로써 해당 Provider가 적절한 인증 과정을 수행할 수 있도록 하는 것이다.

AuthenticicationProvider

실제로 인증 과정을 수행한다. 따라서 각 서비스 시스템에 맞게 해당 적절한 Provider 구현 클래스를 만들어 등록해줄 필요가 있다.

@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    	//전달받은 Authentication 객체로부터 인증 요청 정보를 받는다.
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        MemberContext memberContext = (MemberContext)userDetailsService.loadUserByUsername(username);

		// 실제로 인증 과정을 수행한다.(비밀번호 일치 여부를 판단한다)
        if (!passwordEncoder.matches(password, memberContext.getMember().getPassword())) {
            throw new BadCredentialsException("BadCredentialException");
        }

        FormWebAuthenticationDetails details = (FormWebAuthenticationDetails) authentication.getDetails();
        String secretKey = details.getSecretKey();
        if(secretKey == null || !"secret".equals(secretKey)){
            throw new InsufficientAuthenticationException("시크릿키가 일치하지 않습니다");
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberContext.getMember(), null, memberContext.getAuthorities());
        return authenticationToken; // 인증된 사용자의 정보를 담은 AuthenticationToken을 반환한다.
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

해당 코드 중 실제로 인증을 확인하는 것은
passwordEncoder.matches(password, memberContext.getMember().getPassword) 이 부분이다
PasswordEncoder를 통해 DB에 저장되어 있는 유저의 비밀번호와 해당 사용자가 인증 과정에서 입력한 비밀번호를 비교해 해당 유정에 대한 인증을 수행한다.

Provider의 authenticate 메소드가 Authentication이라는 객체를 매개받아, 다시 Authentication이라는 객체를 반환하는 것을 볼 수 있다.

Authenctication이란, SpringSecuriy의 필터들이 작동할 때 필요한 정보들을 포함하고 있는 하나의 정보 덩어리라고 생각하고 있다. AuthenticationProvider가 매개 변수로 전달받은 Authentication 객체는 아직 인증되지 않은 Authentication 객체이다. Provider는 전달받은 정보를 바탕으로 실제 인증을 수행하고, 만약 인증에 성공한다면 이번엔 인증된 사용자의 정보를 담은 Authentication 객체를 생성해낸 후 해당 객체를 반환한다. 이로써 현재 사용자는 '인증된 사용자'임이 인정되는 것이다.

UserDetailsSerivce

UserDetailsSerivce는 DB에 직접 접근해 해당 사용자의 정보를 조회해오는 역할을 한다.
AuthenticationProvider는 UserDetailsService를 호출해 현재 접근자가 입력한 username을 가지고 이러한 username을 가진 회원이 존재하는지 여부를 확인해와달라고 부탁한다. 만약 해당 username을 가진 회원이 없다면 인증 과정은 그대로 실패끝난다. 만약 해당 username을 가진 회원이 존재한다면, AuthenticationProvider는 이번에는 현재의 접근자가 입력한 password와 UserDetailsService가 DB에서 가지고 온 회원 password를 비교해 해당 사용자의 인증을 수행하게 된다.

인가 절차

다음은 직접 구현한 인증 절차에 대한 간단한 프로세스이다

FilterSecurityInterceptor

인가 프로세스를 이해하는 데 가장 핵심적인 클래스였다. FilterSecurityInterceptor는 최종적으로 현재 접근자가 해당 자원에 접근 가능한지 아닌지를 판단내리는 필터이다.
FilterSecurity는 AccessDecisionManager에게 실제 인가 과정 수행을 위임하고 AccessDecisionManager는 Voter들의 판정 결과들을 종합하여 현재 사용자가 해당 자원에 접근할 수 있는지 없는지 여부를 결정한다.

현재 사용자가 해당 자원에 접근 가능한지 아닌지 판단을 내리기 위해서는 최소한 두 가지의 정보가 필요하다.
첫 번째는 현재 사용자가 어떠한 권한을 가지고 있는지에 대한 정보가 필요하다. 즉 사용자의 권한 정보가 필요하다. 이 정보는 DB에 저장되어 있으며, 앞서 인증 절차에에서 UserDetailsService가 DB에 접근하여 회원 정보를 조회해 오는 과정에서 함께 조회되어 Authentication 객체에 담기게 된다. 따라서 Authentication 객체를 들여다보는 것으로 현재 사용자의 권한 정보를 가지고 올 수 있다.

두 번째로 필요한 것은 현재 사용자가 접근하려 하는 자원에 접근하려면 어떠한 권한이 필요한지에 대한 정보이다. 즉 자원에 접근하기 위해 최소한 어떠한 권한이 필요한지에 대한 정보가 필요하다. FilterSecurityInterceptor는 이 정보를 FilterInvocationSecurityMetadataSource로부터 전달받는다. 자원에 대하 권한 정보를 제공하는 방법에는 여러가지 방식이 존재하며, 따라서 각 방식에 맞는 여러가지 MetadataSource들이 있다.

동적으로 권한 정보를 제공하는 방법! DB에 권한 정보를 저장하자!!

이번 토이 프로젝트에서 나는 동적으로 자원에 대한 권한 정보를 제공하는 방법을 도입해보고자 하였다. 동적으로 정보를 제공한다는 말은 권한 정보 자체가 DB에 저장되어 있어서 필요한 경우에 따라 정보를 수정할 수 있거나, 추가, 삭제가 가능하다는 것을 의미한다.

동적으로 권한 정보를 제공하기 위해 다음과 같은 Entity 관계를 구축했다.

먼저 회원의 정보를 담은 Member 엔티티와 권한 자체에 대한 정보를 가지고 있는 Role엔티티, 그리고 자원에 대한 정보를 가지고 있는 Resource 엔티티가 존재한다.

간단하게 말해
Member 엔티티 => 회원의 이름, 나이, 이메일 ..
Role 엔티티 => 권한의 이름(ROLE_USER, ROLE_MANAGER, ROLE_ADMIN)
Resource 엔티티 => 자원의 url( /post, /post/list, /main 등)
과 같은 정보들이 담겨 있다.

그리고 이 엔티티를 매개할 중간 엔티티로 MemberRole, ResourceRole이 존재한다.
MemberRole은 어떠한 Member(회원)가 어떠한 Role(권한)을 가지고 있는지를 저장하는 테이블이다.
ResourceRole은 어떠한 Resource(자원)에 어떠한 Role(권한)이 필요한지를 저장하는 테이블이다.
이렇게 권한에 정보를 DB에 저장함으로서 얼마든지 CRUD 쿼리를 통해 해당 정보를 변경하고 추가, 삭제할 수 있게 된다.

FilterInvocationSecurityMetadatasource

따라서 FilterInterceptor에게 자원에 대한 권한 정보를 넘기기 위해서 DB에 접근해서 ResourceRole 테이블을 조회해올 수 있는 MetaDatasource가 필요하다.
다음은 직접 구현한 CustomFilterInvocationSecurityMetasource의 일부이다.

@Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        if (requestMap != null) {
            for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()) {
                RequestMatcher matcher = entry.getKey();
                if (matcher.matches(request)) { //인가가 필요한 url이면 어떠한 role을 필요로 하는지 return 한다.
                    return entry.getValue(); // List<ConfigAttribute>를 return 한다.
                }
            }
        }
        return null;
    }

현재 requestMap 이라는 Map 객체에 각각의 url에 접근할 수 있는 권한 정보들이 담겨 있는 상태이다.
HttpServlceRequest에서 현재 사용자가 접근하려고 하는 url을 가지고 온 다음, requestMap에서 해당 url에 필요한 권한 정보 리스트를 뽑아내어 반환하고 있다. FilterInteceptor는 이 정보를 바탕으로 현재 사용자의 접근 가능 여부를 판단할 것이다

SercurityResourceService

DB에서 ResourceRole을 조회해온다.

    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceRoleList(){
        LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
        Optional<List<ResourceRole>> optional = resourceRoleRepository.findAllResourceRole();
        List<ResourceRole> resourceRoles = optional.orElseGet(() -> null);

        for (ResourceRole resourceRole : resourceRoles) {
            RequestMatcher key = new AntPathRequestMatcher(resourceRole.getResource().getUrl());
            
            List<ConfigAttribute> list;
            if(!result.containsKey(key)){
                list = new ArrayList<>();
            }else{
                list = result.get(key);
            }
            list.add(new SecurityConfig(resourceRole.getRole().getName()));
            result.put(key, list);
        }
        return result;

이 반환값이 의미하는 바는 각각의 모든 url에 어떠한 권한이 필요한지에 대한 정보이다.
즉 "/post"에 접근할 수 있는 권한이 ROLE_USER과 ROLE_MANAGER이라고 가정해보자. 이때 하나의 map entry가 return 된다. key값은
new AntPathRequestMatcher("/post")일 것이며, value값은 "ROLE_USER" , "ROLE_MANAGER"이 담긴 리스트일 것이다.

profile
코딩 불바다, 불 같은 코딩, 화끈하게 코딩하자

0개의 댓글