728x90

AuthenticationProvider 커스텀하기

@Component
public class CustomUserAuthenticationProvider implements AuthenticationProvider {   // (1)
    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public HelloUserAuthenticationProvider(CustomUserDetailsService userDetailsService, 
                                           PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }
    
    // ...생략
}

우선 AuthenticationProvider 인터페이스를 구현할 CustomUserAuthenticationProvider 클래스를 생성한다.

 

인증을 처리하기 위해 인증 정보를 조회하고 패스워드를 복호화해줄 때 사용할

기존에 만들어두었던 CustomUserDetailsService와 PasswordeEncoder 객체를 주입받는다.

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

private void verifyCredentials(Object credentials, String password) {
    if (!passwordEncoder.matches((String)credentials, password)) {
        throw new BadCredentialsException("Invalid User name or User Password");
    }
}

우선 supports 메서드를 오버라이딩 하여

CustomUserAuthenticationProvider가 UsernamePasswordAuthentication 방식의

인증을 지원할 것이라고 스프링 시큐리티에 알려준다.

(해당 값이 트루면 해당 클래스의 authenticate 메서드 호출)

 

verifyCredentials 메서드는 이후에 사용할

데이터베이스의 패스워드와 크레덴셜의 패스워드를 비교하는 메서드다.

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;

    String username = authToken.getName();
    Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));

    UserDetails userDetails = userDetailsService.loadUserByUsername(username);

    String password = userDetails.getPassword();
    verifyCredentials(authToken.getCredentials(), password);

    Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

    return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
}

authenticate 메서드는 커스텀한 인증 처리 로직에 따라 사용자 인증 여부를 결정하여

해당 메서드를 오버라이딩 하면 커스텀 인증 처리 로직을 적용할 수 있다.

UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;

String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));

파라미터로 받아온 authentication을 캐스팅하여 UsernamePasswordAuthenticationToken 객체를 얻어서

해당 객체에서 사용자의 Username을 얻어온다.

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password);

userDetailsService에서 Username과 일치하는 사용자 인증 정보를 가져온 후에

가져온 정보에서 패스워드를 가져와 로그인 정보의 패스워드와 비교한다.

Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);

비밀번호까지 일치한다면 인증을 통과한 것이기에

해당 사용자의 권한을 생성한 후에 인증된 사용자의 정보를 리턴한다.

스프링 시큐리티의 예외 처리

try {
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    String password = userDetails.getPassword();
    verifyCredentials(authToken.getCredentials(), password);

    Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
    return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
} catch (Exception ex) {
    throw new UsernameNotFoundException(ex.getMessage());

스프링 시큐리티는 인증 실패 시 AuthenticationException 예외가 발생하지 않으면

예외에 대한 별도의 처리를 하지 않고 서블릿 컨테이너 측으로 넘기기 때문에

위의 코드처럼 기존의 인증 처리 로직을 트라이-캐치 문으로 감싼 후에

발생하는 모든 예외를 잡아서 AuthenticationException을 리턴하거나 상속 받는 예외를 리턴한다.

 

즉, 커스텀 인증을 구현할 때 발생하는 예외는

AuthenticationException을 되던지도록 코드를 짜야한다.

728x90

UserDetailsService

데이터베이스에서 조회한 회원의 인증 정보를 기반으로

인증을 처리하는 커스텀 UserDetailsService를 구현해 보겠다.

 

UserDetailsService는 스프링 시큐리티에서 제공해 주는 컴포넌트로

인증에 필요한 사용자의 정보를 로드하는 인터페이스다.

 

인메모리 데이터베이스를 사용할 때 보았던

InMemoryUserDetailsManager는 UserDetailsManager의 구현체이고

UserDetailsManager는 UserDetailsService를 상속한 인터페이스다.

 

즉, UserDetailsService = UserDetailsManager + UserDetailsService

 

CustomUserDetailsService

@Component
public class MyCustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final CustomAuthorityUtils authorityUtils;
	
    // 의존관계 주입
    public MyCustomUserDetailsService(MemberRepository memberRepository, CustomAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> optionalMember = memberRepository.findByEmail(username);
        Member findMember = optionalMember.orElseThrow(
        	() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)
        );

        Collection<? extends GrantedAuthority> authorities = 
        			authorityUtils.createAuthorities(findMember.getEmail());

        return new User(findMember.getEmail(), findMember.getPassword(), authorities);
    }
}

UserDetailsService 인터페이스를 구현한 커스텀 MyCustomUserDetailsService 클래스를 만든다.

 

데이터베이스에서 사용자 인증 정보를 조회해야 하기 때문에 MemberRepository와

조회한 사용자의 권한 정보를 생성할 CustomAuthorityUtils 클래스를 주입받는다.

(CustomAuthorityUtils 클래스는 아직 구현하지 않았다.)

 

loadUserByUsername 추상 메서드를 구현하기 위해 오버라이딩 한다.

Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(
    () -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)
);

우선 데이터베이스에서 사용자 인증 정보를 조회한다

Collection<? extends GrantedAuthority> authorities = 
		authorityUtils.createAuthorities(findMember.getEmail());

CustomAuthorityUtils의 createAuthorities 메서드를 사용해

역할 기반의 권한 정보 컬렉션을 생성한다.

return new User(findMember.getEmail(), findMember.getPassword(), authorities);

UserDetails 인터페이스의 구현체인 User 클래스의 객체를 통해

조회한 인증 정보와 생성한 권한 정보를 스프링 시큐리티에 제공한다.

 

즉, 데이터베이스에서 인증 정보를 조회해서 권한 정보를 생성한 후에

실제 인증 처리는 스프링 시큐리티에 넘겨준다.

 

UserDetails는 UserDetailsService에 의해 로드되어

인증을 위한 핵심 User 정보를 표현하는 인터페이스다.

@Component
public class CustomAuthorityUtils {
    @Value("${mail.address.admin}")
    private String adminMailAddress;

    private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");

    private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
    
    public List<GrantedAuthority> createAuthorities(String email) {
        if (email.equals(adminMailAddress)) {
            return ADMIN_ROLES;
        }
        return USER_ROLES;
    }
}

MyCustomUserDetailsService에서 역할 기반의 유저 권한을 생성할 때 사용할

CustomAuthorityUtils 클래스의 코드를 살펴보겠다.

@Value("${mail.address.admin}")
private String adminMailAddress;

@Value 어노테이션을 사용하여 yml 파일에 있는 프로퍼티를 가져와서

관리자용 이메일 주소로 정의한다.

private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");

스프링 시큐리티에서 제공하는 AuthorityUtils 클래스를 사용하여

관리자와 유저의 사용 권한 목록을 생성한다.

public List<GrantedAuthority> createAuthorities(String email) {
    if (email.equals(adminMailAddress)) {
        return ADMIN_ROLES;
    }
    return USER_ROLES;
}

createAuthorities 메서드에서는 파라미터로 전달받은 이메일이

프로퍼티로 가져온 관리자용 이메일 주소와 같다면

관리자 계정으로 만들기 위해 관리자 권한을 부여하고

아니라면 일반 권한을 부여한다.

 

학습을 위한 간단한 코드이기 때문에

실제로는 관리자 권한을 이렇게 간단하게 부여하면 안 된다.

리팩토링

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional<Member> optionalMember = memberRepository.findByEmail(username);
    Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

    return new HelloUserDetails(findMember);
}

private final class HelloUserDetails extends Member implements UserDetails {
    HelloUserDetails(Member member) {
        setMemberId(member.getMemberId());
        setFullName(member.getFullName());
        setEmail(member.getEmail());
        setPassword(member.getPassword());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorityUtils.createAuthorities(this.getEmail());
    }

    @Override
    public String getUsername() {
        return getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

기존의 MyCustomUserDetailsService의 loadUserByUsername을

조금 더 유연하게 리팩토링 한 코드이다.

private final class HelloUserDetails extends Member implements UserDetails {
    HelloUserDetails(Member member) {
        setMemberId(member.getMemberId());
        setFullName(member.getFullName());
        setEmail(member.getEmail());
        setPassword(member.getPassword());
    }
    // ...생략
}

기존에 User 객체를 직접 생성하여 리턴하던 역할을 수행하기 위해

UserDetails 클래스를 구현해야 한다.

 

생성자의 파라미터로 데이터베이스에서 조회한 사용자 인증 정보를 받아온다.

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorityUtils.createAuthorities(this.getEmail());
}

역할 기반의 권한 정보 컬렉션 생성하던 코드도 옮겨서 오버라이딩 해준다.

@Override
public String getUsername() {
    return getEmail();
}

getUsername 메서드도 이메일을 리턴하는 것으로 오버라이딩 해준다.

 

이외 나머지 오버라이딩 하는 부분은 당장은 크게 중요하지 않은 부분이기에

true 값을 리턴하도록 설정하였다.

 

코드를 리팩토링 하여 기존에 조회한 사용자 정보를 User 객체로 변환하는 과정과

권한 정보를 생성하는 과정을 캡슐화할 수 있게 되었다.

역할을 데이터베이스에서 관리하기

역할을 데이터베이스에서 관리를 하기 위해서는

일단 역할의 권한 정보를 저장할 테이블을 생성하여

회원 가입 시 사용자의 권한 정보를 저장하거나

로그인 시 사용자의 권한 정보를 조회해야 한다.

@Entity
public class Member extends Auditable implements Principal{
	// ...생략
	@ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles = new ArrayList<>();
	// ...생략
}

우선은 기존의 엔티티의 매핑을 수정해준다.

 

컬렉션 타입의 필드에 @ElementCollection 어노테이션을 추가하면

권한 정보와 관련된 별도의 엔티티 클래스를 생성할 필요가 없다.

private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");

public List<String> createRoles(String email) {
    if (email.equals(adminMailAddress)) {
        return ADMIN_ROLES_STRING;
    }
    return USER_ROLES_STRING;
}

CustomAuthorityUtils 클래스에 역할별 권한을 리턴하는

createRoles 메서드를 추가한다.

public void createMember(Member member) {
    String encryptedPassword = passwordEncoder.encode(member.getPassword());
    member.setPassword(encryptedPassword);
	
    // 추가된 부분
    List<String> roles = authorityUtils.createRoles(member.getEmail());
    member.setRoles(roles);

    memberRepository.save(member);
}

기존에 회원 가입을 처리하던 서비스 메서드에

회원의 권한 정보를 데이터베이스 저장하는 코드를 추가하였다.

 

CustomAuthorityUtils에 만들어두었던 createRoles 메서드를 사용하여

해당 사용자의 권한들을 가져온 후에 데이터베이스에 저장한다.

private final class HelloUserDetails extends Member implements UserDetails {
    HelloUserDetails(Member member) {
        setMemberId(member.getMemberId());
        setFullName(member.getFullName());
        setEmail(member.getEmail());
        setPassword(member.getPassword());
        setRoles(member.getRoles());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorityUtils.createAuthorities(this.getRoles());
    }
}

회원가입은 수정했으니 로그인도 위의 코드처럼 수정하면 된다.

 

기존 멤버 엔티티의 수정자 메서드를 사용하던 부분에

권한 정보를 추가하는 수정자 메서드를 추가한다.

 

기존에는 이메일로 권한 목록을 생성하던 createAuthorities 메서드의

파라미터도 데이터베이스에서 가져온 역할로 권한 목록을 생성하게 수정한다.

public List<GrantedAuthority> createAuthorities(List<String> roles) {
   List<GrantedAuthority> authorities = roles.stream()
           .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // (2)
           .collect(Collectors.toList());
   return authorities;
}

바꾼 파라미터에 맞게 createAuthorities 메서드를 오버라이딩 한다.

728x90

패스워드 인코더

클라이언트로부터 받아온 패스워드 같은 민감한 정보는

받아온 그대로 데이터베이스에 저장하는 것이 아니라

저장하기 전에 암호화해줘야 한다.

 

스프링 시큐리티에서는 이러한 암호화 기능을 제공해 주기 때문에

PasswordEncoder 클래스를 사용기만 하면 된다.

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

우선 PasswordEncoder의 구현 객체를 만들기 위해

PasswordEncoderFactories의 createDelegatingPasswordEncoder 메서드를 사용하여

DelegatingPasswordEncoder 구현 객체를 만든다.

@Configuration
public class JavaConfiguration {
    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, 
                                               PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager, passwordEncoder);
    }
}

Configuration 클래스를 하나 추가한 후에

멤버 서비스 인터페이스의 구현 객체를 빈으로 등록해 준다.

 

이때 멤버 서비스 객체에서는 회원가입, 즉 회원을 생성하는 기능을 수행하기 때문에

암호화된 패스워드를 등록하기 위해 PasswordEncoder 객체를 주입받게 한다.

 

UserDetailsManager 객체는 학습을 위해 인메모리 DB를 사용하기 위해 추가했다.

유저 등록 (인메모리 DB 사용)

public Member createMember(Member member) {
    List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());

    String encryptedPassword = passwordEncoder.encode(member.getPassword());

    UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);

    userDetailsManager.createUser(userDetails);

    return member;
}

위와 같이 멤버 서비스 클래스에 회원가입을 처리하는 메서드를 추가한다.

List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());

한 줄씩 살펴보면 우선 GrantedAuthority 객체를 담는 리스트를 선언하고

createAuthorities 메서드를 통해 받아온다.

 

GrantedAuthority는 회원의 권한으로 회원을 등록하기 위해서는 권한을 지정해야 한다.

 

관리자가 아닌 일반회원의 회원가입을 진행하기 위해 일반회원의 권한을 가져온다.

private List<GrantedAuthority> createAuthorities(String... roles) {
    return Arrays.stream(roles)
            .map(role -> new SimpleGrantedAuthority(role))
            .collect(Collectors.toList());
}

createAuthorities 메서드의 로직은 위와 같다.

 

SimpleGrantedAuthority 생성자의 파라미터로 회원의 역할을 전달하여

객체를 생성한 후에 리스트 형태로 리턴한다.

 

이때 SimpleGrantedAuthority를 사용해 역할 기반 형태의 권한을 지정하려면

ROLE_권한명 형태로 지정해야 적절한 권한 매핑이 이루어진다.

String encryptedPassword = passwordEncoder.encode(member.getPassword());

PasswordEncoder의 encode 메서드를 사용하여

받아온 회원가입 정보에서 비밀번호를 암호화한다.

UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);

User 생성자의 파라미터로 이메일과 암호화된 패스워드와 권한을 전달하고

User 정보를 관리하기 위해 UserDetails를 생성한다.

 

이때 이전 단계에서 패스워드를 암호화하지 않으면 등록은 할 수 있지만

 

userDetailsManager.createUser(userDetails);

UserDetailsManager의 createUser 메서드를 사용하여 User를 등록한다.

유저 등록 (DB 사용)

인메모리 DB를 통해 회원등록이 어떻게 진행되는지 학습했으니

실제 데이터베이스를 사용하여 구현해 보겠다.

@Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
        		// 추가 부분
                .headers().frameOptions().sameOrigin()
                // 
                .and()
                .csrf().disable()
                .formLogin()
                .loginPage("/auths/login-form")
                .loginProcessingUrl("/process_login")
                .failureUrl("/auths/login-form?error")
                .and()
                // 로그아웃 시작
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                // 로그아웃 종료
                .and()
                .exceptionHandling().accessDeniedPage("/auths/access-denied")
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers("/orders/**").hasRole("ADMIN")
                        .antMatchers("/members/my-page").hasRole("USER")
                        .antMatchers("⁄**").permitAll()
                );

        return httpSecurity.build();
    }

우선 기존의 필터체인의 코드를 위와 같이 수정한다.

 

frameOptions 메서드는 HTML의 frame, iframe, object 태그에서

페이지의 랜더링을 허용할지 여부를 결정할 때 사용한다.

 

스프링 시큐리티에서는 기본값으로 클릭재킹 공격을 막기 위해

랜더링을 허용하지 않으려 DENY를 적용한다.

 

sameOrigin() 메서드를 사용하면 동일 출처로부터 들어오는 요청만 랜더링을 허용하는데

현재 코드에서는 H2 데이터베이스를 편리하게 사용하기 위해 부분적으로 허용한다.

@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
                                     PasswordEncoder passwordEncoder) {
    return new DBMemberService(memberRepository, passwordEncoder);
}

이전에는 인메모리 방식의 회원등록을 위해서 UserDetailsManager를 사용하였지만

실제 데이터베이스를 사용할 것이기 때문에 MemberRepository를 추가로 주입받는다.

public void createMember(Member member) {
    String encryptedPassword = passwordEncoder.encode(member.getPassword());
    member.setPassword(encryptedPassword);

    memberRepository.save(member);
}

인메모리DB를 사용하던 createMember 메서드와 마찬가지로

PasswordEncoder를 사용해 암호화된 비밀번호로 수정한 후에

해당 회원을 데이터베이스에 저장하면 된다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 커스텀 로그인 인증 구현  (0) 2023.07.11
[스프링 시큐리티] 커스텀 인증 처리 구현  (0) 2023.07.11
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
728x90

스프링 시큐리티

스프링 MVC/WebFlux 기반 애플리케이션의 인증인가 기능을 지원하는 보안 프레임워크

 

주로 아래와 같은 기능들을 제공한다.

  • 다양한 유형의 사용자 인증 기능 적용 (폼 로그인, 토큰 기반, OAuth2 기반 등의 인증)
  • 사용자의 역할에 따른 권한 레벨 적용
  • 리소스에 대한 접근 제어
  • 민감한 정보의 데이터 암호화
  • SSL 적용
  • 일반적으로 알려진 공격 차단
  • SSO
  • 클라이언트 인증서 기반 인증
  • 메서드 보안
  • 접근 제어 목록

위에 언급한 것 외에도 많은 기능들을 제공한다.

 

사용해 보기

implementation 'org.springframework.boot:spring-boot-starter-security'

gradle 설정에 스프링 시큐리티 의존성을 추가해 준다.

해당 의존성만 추가한 후에 서버를 실행시키면

별도의 설정 없이도 위와 같은 로그인 창이 만들어진다.

2023-07-10 11:29:25.816  WARN 5668 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 32548629-b669-4cb1-af28-6595d2914bfc

This generated password is for development use only. Your security configuration must be updated before running your application in production.

로그인을 위해서는 기본 값으로는

Username에 user를 적고

Password에는 콘솔창에 나오는 비밀번호를 사용하면 된다.

@Configuration
public class SecurityConfiguration {

    @Bean
    public UserDetailsManager userDetailsManager() {
        UserDetails userDetails =
                User.withDefaultPasswordEncoder()
                        .username("da9dac")
                        .password("root")
                        .roles("USER")
                        .build();

        return new InMemoryUserDetailsManager(userDetails);
    }
}

하지만 기본 비밀번호는 서버를 껐다 킬 때마다 바뀌기 때문에

위와 같은 작업을 통해 Username과 Password를 고정시킬 수 있다.

 

당장은 학습용으로만 사용할 것이기 때문에

InMemoryUserDetailsManager를 사용한다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
    		// CSRF 공격에 대한 설정 비활성화 (학습을 위해 비활성화)
            .csrf().disable() // 기본값은 enable
            .formLogin() // 인증 방식을 폼 로그인 방식으로 지정
            .loginPage("/auths/login-form") // 로그인 페이지로 사용할 템플릿
            .loginProcessingUrl("/process_login") // 인증 요청을 수행할 요청 URL
            .failureUrl("/auths/login-form?error") // 실패할 경우 사용할 템플릿
            .and()
            .authorizeHttpRequests() // Http 요청이 들어오면 접근 권한을 확인
            .anyRequest().permitAll(); // 클라이언트의 모든 요청에 대한 접근 허용

    return httpSecurity.build();
}

만약 별도의 커스텀 로그인 페이지를 사용하고 싶다면 위와 같은 코드를

SecurityConfiguration 클래스에 추가해 주면 된다.

 

해당 메서드의 파라미터로 받아오는 HttpSecurity 클래스는

HTTP 요청에 대한 보안 설정을 구성하는 클래스다.

 

해당 객체를 이용해 자신에게 필요한 보안 옵션을 적용할 수 있다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .csrf().disable()
            .formLogin()
            .loginPage("/auths/login-form")
            .loginProcessingUrl("/process_login")
            .failureUrl("/auths/login-form?error")
            .and()
            .exceptionHandling().accessDeniedPage("/auths/access-denied")
            .and()
            .authorizeHttpRequests(
                    authorize -> authorize
                            .antMatchers("/orders/**").hasRole("ADMIN")
                            .antMatchers("/members/my-page").hasRole("USER")
                            .antMatchers("⁄**").permitAll()
            );

    return httpSecurity.build();
}

위의 코드는 기존의 코드에서 exceptionHandling 메서드를 추가하고

authorizeHttpRequests 메서드의 파라미터 부분만 바꾼 코드이다.

.exceptionHandling().accessDeniedPage("/auths/access-denied")

우선 exceptionHandling 메서드는 예외를 처리하는 기능을 하는데

ExceptionHandlingConfigurer를 리턴하고 이를 통해 예외 처리를 할 수 있다.

 

ExceptionHandlingConfigurer객체의 accessDeniedPage 메서드를 사용해

403 에러가 발생한 경우 지정한 URL로 리다이렉트 되도록 했다.

.authorizeHttpRequests(
        authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry
                    .antMatchers("/orders/**").hasRole("ADMIN")
                    .antMatchers("/members/my-page").hasRole("USER")
                    .antMatchers("⁄**").permitAll()
);

authorizeHttpRequests 메서드는 람다식을 사용해 request URI에 접근 권한을 부여할 수 있다.

 

antMatcher 메서드와 hasRole 메서드를 사용하여 페이지 별로 권한을 정할 수 있는데

antMatcher 메서드의 파라미터에는 어떤 URI를 매치할지 정하고

hasRole 메서드의 파라미터에는 어떤 권한만 허용할지 지정한다.

 

antMatcher 메서드에서 "/orders/**"와 같은 부분은

orders라는 URL의 하위 모든 URL을 포함하지만

만약 "/orders/*" 라면 orders의 깊이가 1인 URL까지만 포함한다.

.antMatchers("⁄**").permitAll()

마지막 부분은 위에서 지정한 모든 URL 이외의 URL의 권한을 정하는 부분으로

이외의 모든 URL은 모두가 접근이 가능하다는 의미다.

 

antMatchers 메서드의 순서에 따라 우선순위가 적용되기 때문에

구체적인 URL 경로부터 접근 권한을 부여한 후에

마지막 부분처럼 모든 URL을 처리하는 부분은 가장 나중에 적용해야 한다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .formLogin()
        .loginPage("/auths/login-form")
        .loginProcessingUrl("/process_login")
        .failureUrl("/auths/login-form?error")
        .and()
        // 로그아웃 시작
        .logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/")
        // 로그아웃 종료
        .and()
        .exceptionHandling().accessDeniedPage("/auths/access-denied")
        .and()
        .authorizeHttpRequests(authorize -> authorize
                .antMatchers("/orders/**").hasRole("ADMIN")
                .antMatchers("/members/my-page").hasRole("USER")
                .antMatchers("⁄**").permitAll()
        );
    return http.build();
}

로그인에 대한 기능을 추가했으면 로그아웃도 필요하다.

 

위의 코드에서 로그아웃 설정을 위해 logout 메서드를 호출하여 LogoutConfigurer를 리턴하는데

logoutUrl 메서드를 사용해 로그아웃에 사용할 요청 URL을 지정해 주고

logoutSuccessUrl 메서드를 사용해 로그아웃 성공 시 리다이렉트 할 URL을 지정해 준다.

 

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 커스텀 인증 처리 구현  (0) 2023.07.11
[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
Hashing  (0) 2023.07.07
728x90

세션 기반 인증

세션 : 사용자가 인증에 성공한 상태로 세션 스토어나 인메모리 DB에 저장 됨

세션 아이디 : 각 세션을 구분할 수 있는 기본키 같은 역할

 

로그인

1. 만약 사용자가 로그인을 요청한 후에 성공했을 경우

서버는 세션을 만들어 저장한 후에 세션의 아이디를 클라이언트에 전달

 

2. 로그인을 유지할 수단으로 쿠키를 사용하며

이때 쿠키에 세션의 아이디를 저장해둔다.

 

3. 쿠키에 저장된 세션 아이디를 서버에 전달하면서

세션 스토어에 저장하는 세션이면 로그인 상태가 검증되기 때문에

쿠키와 세션을 사용해 로그인 상태를 유지할 수 있다.

 

로그아웃

세션은 로그인 성공에 대한 증명 정보를 가지고 있는

일종의 서버 측에 자유롭게 드나들 수 있는 프리패스권과 같은 개념이다.

 

그래서 로그아웃을 하기 위해서는 서버 측에서는 해당 세션의 정보를 삭제하고

클라이언트 측에서는 쿠키를 갱신하면 된다.

 

서버가 클라이언트의 쿠키를 삭제할 수는 없기 때문에

set-cookie를 사용해 클라이언트에 쿠키를 전송할 때

세션 아이디의 키값을 무효한 값으로 갱실할 수 있다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Cookie  (0) 2023.07.07
Hashing  (0) 2023.07.07
HTTPS  (0) 2023.07.07
728x90

쿠키는 서버에서 클라이언트에 데이터를 저장하는 방법으로

서버에서 클라이언트에 쿠키를 저장하는 것만 의미하는 것이 아니라

클라이언트에서 서버로 쿠키를 전송하는 것도 포함된다.

 

쿠키의 특징

 

서버가 클라이언트에 데이터를 저장할 수 있지만 가져올 때는 조건이 있다

쿠키 옵션을 만족하는 경우에만 클라이언트로부터 데이터를 가져올 수 있다.

 

다양한 쿠키 옵션이 존재하지만 주로 사용되는 것들은

Domain, Path, MaxAge, Expires, Secure, HttpOnly, SameSite 등이 있다.

 

Domain

https://n.news.naver.com/mnews/article/003/0011960011?sid=104

도메인은 서버에 접속할 수 있는 이름을 말하며

위의 주소에서는 naver.com 부분이 도메인에 해당하고

n.news 부분을 서브 도메인이라고 한다.

 

쿠키 옵션에 도메인 정보가 존재한다면 클라이언트는

쿠키와 서버 양쪽의 도메인이 일치해야만 쿠키를 전송할 수 있다.

 

Path

서버가 라우팅 할 때 사용하는 경로로

mnews/article 이런 부분들에 해당한다.

 

만약 Path가 mnews로 설정되어 있다면

해당 패스 뒤에 오는 추가 패스들에도 쿠키 전송이 가능하다

즉, mnews/article 은 가능하지만 xnews/article 같은 경우는 패스가 다르기 때문에 안된다.

 

MaxAge / Expires

쿠키가 언제까지 유효하게 할지 기간을 정하는 옵션이다.

 

MaxAge는 앞으로 몇 초 동안 쿠키가 유효한지 설정한다면

Expires는 언제까지 유효한지 Date를 지정한다.

 

해당 옵션들이 없는 쿠키는 브라우저가 실행 중일 때마 사용할 수 있고

브라우저 종료 시 삭제되기 때문에 세션 쿠키라고 한다.

 

해당 옵션들이 적용되면 지정된 기간만큼 사용가능한 쿠키이기 때문에

영속성 쿠키라고 한다.

 

Secure

쿠키를 전송해야 할 때 사용하는 프로토콜에 따른 전송 여부를 결정할 때 사용한다.

 

해당 옵션이 true라면 HTTPS 프로토콜을 사용한 통신만 가능하며

false라면 모든 프로토콜의 통신이 가능하다.

 

HttpOnly

자바 스크립트에서 브라우저의 쿠키에 접근을 허용할지 결정할 때 사용한다.

 

자바 스크립트에서 쿠키에 접근을 하는 것은 XSS 공격에 취약하기 때문에

기본값인 false 대신 true로 설정하여 접근하지 못하게 하는 것이 좋다.

 

SameSite

Cross-Site 요청을 받은 경우에 요청에서 사용한 메서드와 해당 옵션의 조합을 기준으로

서버의 쿠키 전송 여부를 결정한다.

Cross-Origin : 도메인, 프로토콜, 포트 중 하나라도 다른 경우
Cross-Site : eTLD+1이 다른 경우로, TLD는 .com, .org 같은 최상위 도메인을 말하며,
최상위 도메인의 왼쪽 하위 레벨의 도메인을 합친 것을 eTLD+1라고 한다.

https://da9dac.github.io
https://da8dac.github.io
io 같은 경우는 github.io를 하나의 TLD로 보기 때문에
eTLD+1에 해당하는 부분은 da9dac과 da8dac다.

위의 주소들은 eTLD+1가 서로 다르기 때문에 Cross-Site에 해당한다.

SameSite의 옵션은 아래와 같다.

  • Lax : Cross-Site 요청이라면 GET 메서드에 대해서만 쿠키를 전송
  • Strict : SameSite 요청인 경우에만 쿠키를 전송
  • None : 항상 쿠키를 전송하지만 보안에 취약하여 Secure 옵션 필요

 

쿠키를 이용한 상태 유지

HTTP는 무상태성을 가지지만 이러한 쿠키의 특성을 이용하여상태를 가지고 통신을 할 수 있게 만들 수 있다.

 

하지만 쿠키는 오랜 시간 유지될 수 있고, 자바 스크립트를 통해 접근할 수 있어보안에 취약한 점이 있기 때문에 민감한 정보를 담는 것은 좋지 않다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Hashing  (0) 2023.07.07
HTTPS  (0) 2023.07.07
728x90

복호화가 불가능하고 암호화만 가능한 암호화 방식

 

해시 함수를 사용하여 암호화를 진행하며 아래와 같은 특성이 있다.

  • 항상 같은 길이의 문자열을 리턴한다
  • 문자열에 해시 함수 적용 시 서로 다른 문자열은 항상 다른 결과를 반환한다.
  • 반대로 서로 같은 문자열은 항상 같은 결과를 반환한다.
  • 같은 문자라면 결과가 항상 같다는 것

레인보우 테이블

항상 같은 결과를 반환하는 특성을 이용해 해시 함수 적용 전의 값을

알아낼 수 있도록 기록해두는 테이블

 

이 테이블이 유출되는 경우 해싱이 되었더라도 이전의 값을 알아낼 수 있어 위험하다.

 

솔트

단어 그대로 소금을 치듯 해싱 이전 값에 임의의 값을 더하여

데이터가 유출되어도 데이터를 알아내기 어렵게 만드는 방법

 

해싱 값이 유출되더라도 솔트가 유출되는 것이 아니라면 안전함

 

복호화가 불가능한 이유

해싱은 데이터를 사용하는 것이 목적이 아닌

데이터가 동일한지만 확인하는 것이 목적이기 때문에

같은 해싱값이라면 동일한 데이터이니 굳이 복호화를 할 필요가 없다.

 

그래서 비밀번호와 같은 데이터가 동일한지만 판단하면 되고

데이터 유출의 위험성이 없어야하는 부분에 대해서는

주로 해싱 기법을 사용한다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
HTTPS  (0) 2023.07.07
728x90

Hyper Text Transfer Protocol + Secure Socket layer의 약자로

기존의 HTTP에 보안 기능을 추가한 계층이라고 볼 수 있다.

 

HTTP 요청을 SSL 혹은 TLS라는 알고리즘을 이용해

통신 과정에서 데이터를 암호화하여 전송하는 방법이다.

 

HTTPS의 장점

암호화

서버와 클라이언트가 주고 받는 정보를 중간에 제 3자가 탈취할 수 없도록

정해둔 방법으로 데이터를 암호화하여 주고 받기 때문에

데이터가 탈취되어도 데이터를 알아볼 수 없다.

 

클라이언트와 서버 간의 데이터는 비대칭키와 대칭키 방식을 혼용해서 사용하기 때문에

데이터를 복호화하기 전까지 제 3자는 데이터를 알 수가 없기 때문이다.

 

클라이언트와 서버 간의 데이터를 주고 받을 때는 양쪽이 비밀 키를 공유하여

데이터를 암호화하고 복호화 하는 대칭키 방식을 사용하고

대칭키를 주고 받을 때는 비대칭키를 사용한다.

 

대칭키는 비밀 키를 사용하여 복호화가 쉽기 때문에 클라이언트와 서버 간에 사용하지만

대칭키가 탈취되는 경우에는 복호화 될 가능성이 높기 때문에

대칭기를 주고 받는 과정에서는 개인이 가진 비밀 키로만 풀 수 있는

비대칭키 방식으로 주고 받아 복호화 할 수 없게 한다.

 

  • 대칭키 : 하나의 비밀 키를 서로 공유하여 암호화와 복호화에 사용함
  • 비대칭키 : 공개 키를 사용하여 암호화를 하지만 복호화는 개인 키로만 가능함

 

인증서

브라우저가 서버의 응답과 같이 전달된 인증서를 확인할 수 있기 때문에

서버의 신원을 보증해준다.

 

이러한 서버의 신원을 보증해주는 제 3자를 CA(Certificate Authority)라고 부르며

인증서를 발급해 주는 공인 기관들로 서버의 공개키와 정보를 비밀키로 암호화하여 인증서를 발급한다.

 

인증서를 통해 검증하는 과정은 아래와 같다.

  1. 서버가 클라이언트에 인증서를 전달
  2. 클라이언트 측의 운영체제나 브라우저에 내장된 CA 리스트에서 조회
  3. 인증된 CA에서 발급 받은 인증서인지 확인
  4. 아니라면 경고창을 띄워서 연결이 안전하지 않다 알림
  5. 인증된 인증서라면 해당 CA 기관의 공개키로 인증서를 복호화
  6. 복호화해 얻은 공개키로 서버가 신뢰할 수 있는 대상인지 판단

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
Hashing  (0) 2023.07.07

+ Recent posts