Spring

[Spring] Spring Security - 주요 모듈

da77777 2022. 6. 13. 22:27

Spring Security

Spring 기반 애플리케이션의 인증로그인, 인가 등을 담당하는 스프링 하위 프레임워크

필터 기반으로 동작 → 인증, 인가를 필터 흐름에 따라 처리

보안과 관련하여 다양한 옵션을 제공해주고 커스텀도 가능하기 때문에 개발자의 편의성을 높여줌

근데 처음 마주하는 순간에는 세상에서 제일 막막한 사람 될 수 있음

 

관련 용어

  • Resources (리소스) : 접근 주체가 접근하고자 요청하는 자원 (글 작성 페이지, 관리자 페이지 등)
  • Principal(접근 주체) : 리소스에 접근하려는 사용자
  • Credential(비밀번호) : 리소스에 접근하는 대상의 비밀번호
  • Authentication(인증) : 접근 주체가 해당 어플리케이션을 사용할 수 있는 사용자인지를 확인
    쉽게 말하면 로그인 했을 때 통과 되는 사용자인지를 확인하는 것
  • Authorization(인가) : 인증된 사용자가 리소스에 접근할 수 있는 권한을 가졌는지 확인

 

Spring Security 흐름

  1. 사용자(접근 주체)가 로그인을 시도 (Http Request)
  2. AuthenticationFilter에서 UsernamePasswordAuthenticationToken의 첫 번째 생성자에 id, pw 정보를 담아서 
  3. AuthenticationManager ~ UserDetailsService 까지 전달
  4. UserDetailsService의 loadUserByUsername 메소드를 통해 DB에서 사용자의 정보 조회
  5. DB에 존재하는 사용자일 경우 UserDetails 객체를 만들고 AuthenticationProvider에 넘겨줌
  6. AuthenticationProvider에서 인증(비밀번호 매칭 등)에 성공하면 사용자 정보가 담긴 Authentication 객체를 AuthenticationFilter까지 전달
  7. AuthenticationFilter가 전달받은 UsernamePasswordAuthenticationToken을 LoginSuccessHandler로 보내면
  8. Authentication 객체를 SecurityContext에 저장
  9. 사용자에게 sessionID 부여
  10. 로그인 성공한 후 사용자가 요청할 때마다 쿠키에서 JSESSIONID를 확인하고 유효하면 Authentication 처리해줌

 

 

주요 모듈

* 로직은 [더보기]

SecurityContextHolder

보안 주체의 세부 정보 및 응용 프로그램의 보안 컨텍스트에 대한 세부정보를 저장

기본적으로 ThreadLocal을 사용하며, SecurityContext를 감싸고 있음

ThreadLocal
1. 한 쓰레드 내에서 공유하는 저장소.  해당 쓰레드의 SecurityContext에 담긴 정보는 애플리케이션 어디에서든 접근 가능
2. ThreadLocal을 사용한다는 것은 각 쓰레드들이 각자의 SecurityContextHolder를 가지고 있다는 뜻이기도 하며, 이로 인해 사용자 별로 각각의 인증 객체(Authentication)를 가질 수 있는 것

의문 : Thread 생성에는 한계가 있을 텐데 이건 어떻게 처리되는가?

 

SecurityContext

Authentication을 저장하고 있음

ThreadLocal에 저장됨 (SecurityContextHolder가 ThreadLocal역할)

 

Authentication

접근 주체의 정보(+권한)을 담는 인터페이스

Authentication객체는 SecurityContext를 통해 접근할 수 있으며

SecurityContext 는 SecurityContextHolder를 통해 접근할 수 있음

SecurityContextHolder.getContext().getAuthentication()

 

더보기
package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials(); //보통 PW

    Object getDetails(); //상세 정보

    Object getPrincipal(); //보통 ID

    boolean isAuthenticated(); //인증 성공 여부

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

 

 


UsernamePasswordAuthenticationToken

user ID가 Principal역할, user PW가 Credential 역할

첫 번째 생성자는 인증 전, 두 번째 생성자는 인증 후의 객체를 생성할 때 사용됨

principal - user ID / credential - user pw

더보기
package org.springframework.security.authentication;

import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 560L;
    private final Object principal;
    private Object credentials;

	//인증 되기 전의 객체를 생성할 때 사용
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

	//인증 완료 후의 객체를 생성할 때 사용
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

 


AuthenticationManager

인증에 대한 부분을 처리하는 부분

(근데 실질적으로는 AuthenticationManager가 호출하는 AuthenticationProvider에서 처리함)

AuthenticationManager의 authenticate()를 통해 인증이 되면 isAuthenticated(boolean)값이 true로 변경됨

AutheticationProvider에서 인증 성공 시 해당 접근 주체의 정보가 담인 authentication 객체가 SecurityContext에 담김 

더보기
package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

 

AuthenticationProvider

실질적으로 인증에 대한 부분을 처리

인증 되기 전의 Authentication 객체를 받았다가 인증이 완료 된 객체를 return

인증 완료 시 UsernamePasswordAuthenticationToken의 두 번째 생성자를 사용하여 인증 성공한 접근 주체의 정보를 담고

AuthenticationProvider를 호출한 AuthenticationManager에 넘겨주게 됨

인증 시 체크해야 하는 부분들(활동정지, 탈퇴회원 여부 검증 등)을 커스텀하고자 하면

AuthenticationProvider를 inplements하고 구현해주면 됨

 

커스텀 시 예외처리는 AutenticationException을 사용

AutenticationException의 종류
  • BadCredentialsException : 비밀번호 불일치
  • UsernameNotFoundException : 계정없음
  • AccountExpiredException : 만료된 계정
  • CredentialsExpiredException : 만료된 비밀번호
  • DisabledException : 비활성화 된 계정
  • LockedException : 잠긴 계정
더보기
package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}
더보기
//AuthenticationProvider을 구현한 class
...

@Log
public class CustomAutheticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoder bCryptPwd; //비밀번호 암호화

    //Authentication authentication : AuthenticationManager로부터 전달받은 인증객체로 username, password가 담겨 있음
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String email = authentication.getName();
        String password = (String) authentication.getCredentials(); //Credentials : 비밀번호

        // 인코딩 된 password 일치 여부, 활동정지 여부, 탈퇴 여부 등을 체크
        // 인증 실패 시 AuthenticationException 중에서 적절한 Exception 선택하여 Exception 발생시킴

		...

        //인증 완료 후 authenticationToken에 회원 정보(비밀번호 제외한 회원 정보)를 담아 AuthenticationProvider를 호출한 AuthenticationManager에 return
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberAccount, null, memberAccount.getAuthorities());
        return authenticationToken;
    }

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

 

AuthenticationProvider 커스텀 후 SecurityConfig에 Bean 등록

...
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	...
    @Bean
    public AuthenticationProvider customAuthenticationProvider() {
        return new CustomAutheticationProvider();
    }
	...
}

 


 

UserDetailsService

DB에서 조회한 사용자 정보를 담은 UserDetails 객체를 반환하는 메소드를 가짐(loadUserByUsername)

더보기
package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
더보기
//UserDetailsService를 구현한 class
...
public class CustomMemberDetailsService implements UserDetailsService {

    @Autowired
    private MemberService memberService;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        //view에서 받은 email(ID)을 사용해 사용자 정보가 있는지 조회하는 memberService 메소드
        MemberVO memberVo = memberService.retrieveMemberByEmail(email);

        //정보 없으면 예외 처리
        if(memberVo == null) {
            throw new UsernameNotFoundException("UsernameNotFoundException, memberVo == null");
        }
	...
    	// 권한 추가하는 로직
	...
        return new MemberAccount(memberVo, roles); //MemberAccount : User를 상속받은 class
    }
}

 

UserDetails

인증에 성공하면 생성되는 UserDetails 객체

Authentication객체를 구현한 UsernamePasswordAuthenticationToken을 생성할 때 사용

더보기
package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

 


참고

https://mangkyu.tistory.com/76

 

[SpringBoot] Spring Security란?

대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능

mangkyu.tistory.com

https://sjh836.tistory.com/165

 

spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal,

참조문서 https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-started http://springsource.tistory.com/80 https://okky.kr/article/382738 1. 스프링 시큐리티란?..

sjh836.tistory.com

https://www.inflearn.com/questions/209694

 

그럼 SecurityContext가 저장되는 곳은 총 3곳인건가요? - 인프런 | 질문 & 답변

처음에 Authentication객체를 SecurityContext에 담아서 보관한다는것 까지능 이해했습니다. 그럼 SecurityContext가 저장되는 곳이 1. ThreadLocal 2. HttpSession 3. SecurityContextHolder 총 3개의 공...

www.inflearn.com