부자 되기 위한 블로그, 머니킹

Spring Security 개념

대부분의 웹 프로젝트는 인증 시스템을 가지고 있다. 작게는 경로를 설정해두었지만 특정한 사람만 들어오도록 하는 것이고 많이 쓰이는 인증 시스템으로는 로그인 한 유저만 볼 수 있는 페이지를 만드는 것이다.

spring security는 spring 기반의 보안 담당 프레임워크이다. 인증과 권한에 대한 부분을 filter의 흐름에 따라 처리하고 있다. 경로에 대한 Role 설정 및 로그인 시스템 및 페이지 등을 자동으로 처리해주어 편하다.



Spring Security build.gradle

먼저 spring security를 사용하기 위해서는 gradle을 추가해야 한다.

// web security 관련
    implementation 'org.springframework.boot:spring-boot-starter-security'



Spring Security Config Class

기본적인 security 설정 클래스이다. 어노테이션으로 Configuration 과 EnbaleSecurity가 꼭 들어가줘야 한다.

package com.jelly_develop.passprofile.role.login;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/templates/**\",\"/resources/**","/css/**","/image/**","/js/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.headers().frameOptions().sameOrigin();
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/[경로]").permitAll()
                .antMatchers("/[경로]").permitAll()
                .antMatchers("/[경로]/**").permitAll()
                .antMatchers("/[경로]").hasRole("MEMBER")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/[경로]")
                .loginProcessingUrl("/[경로]")
                .defaultSuccessUrl("/")
                .and()
                .logout();

    }
}

Security Configuration에 관하여

  • frameOptions을 통해 iframe 관련 설정을 할 수 있다. 기타 header 설정 또한 가능하다.
  • antMatchers를 통해 해당 경로에 대한 접근 설정을 할 수 있다.
  • and()를 통해 다음 auth 관련 설정을 할 수 있다.
  • formLogin의 loginPage를 통해 인증이 필요한 페이지에 접근 시 자동으로 로그인 페이지로 이동한다.
  • loginProcessingUrl은 로그인 진행 시 어떤 경로에서 인증 시스템을 진행할 것인지를 설정하는 메소드이다.
    • 스프링에서는 기본 경로가 /login으로 설정되어 있으며 이 프로세스는 UserDetailsService로 향한다.
    • 기본으로 놔두는 것이 초심자에게 좋은 일이다.
  • defaultSuccessful의 경우 성공시 가는 경로이다.



Spring UserDetails

package com.jelly_develop.passprofile.role.login;
import com.jelly_develop.passprofile.domain.member.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

//Security Session => Authentication => UserDetails
public class PrincipalDetails implements UserDetails {

    private Member member; // 콤포지션

    public PrincipalDetails(Member member) {
        this.member = member;
    }

    // 해당 USER의 권한을 return하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> auth = new ArrayList<>();
        auth.add(new SimpleGrantedAuthority(member.getRole()));

        return auth;
    }

    @Override
    public String getPassword() {
        return member.getPassword();
    }

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

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

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

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

    @Override
    public boolean isEnabled() {
        // 우리 사이트에서 1년동안 회원이 로그인을 안하면 휴면계정으로 하기로 했다면
        return true;
    }
    //private final MemberRepository memberRepository;

}

###Spring Security 인증 진행 순서

  1. 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인 진행한다.
  2. 로그인 진행이 완료되면 시큐리티 session을 만들어줍니다 (Security ContextHolder)
  3. 인증 오브젝트 Authentication 타입 객체입니다.
  4. Authentication 안에 User 정보가 있어야됩니다.
  5. User 오브젝트타입은 UserDetails 타입 객체로 커스텀을 하려면 상속받아 작성해야 합니다.
  6. getAuthorities 메소드는 인증하는 User가 어떤 종류의 권한을 가지고 있는지를 설정합니다.
  7. 그 외에 is 관련 메소드는 인증 시스템에 관련된 세부 설정이라고 볼 수 있습니다.



UserDetailsService

UserDetailsService는 인증 프로세스와 같으며 loadUserByUsername을 override 인증 프로세스를 진행합니다.

package com.jelly_develop.passprofile.role.login;

import com.jelly_develop.passprofile.domain.member.Member;
import com.jelly_develop.passprofile.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Slf4j
public class PrincipalDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

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

        Member memberEntity = memberRepository.findByEmail(username);
        if (memberEntity != null) {
            System.out.println("memberEntity = " + memberEntity.getEmail());
            return new PrincipalDetails(memberEntity);
        }
        return null;
    }
}

security 인증 시스템 작동 방식

  • 시큐리티 설정에서 loginProcessingUrl("/login");
  • login 요청이 오면 자동으로 UserDetailsService 타입으로 IOC 되어있는 loadUserByUsername 함수가 실행됨
  • 시큐리티 session(내부 Authentication(내부 UserDetails) = Authentication = UserDetails
  • form 타입에서 string parameter 변수 이름은 name과 일치해야된다.



html form

<div class="wrapper">
    <div class="logo"> <img src="https://www.freepnglogos.com/uploads/twitter-logo-png/twitter-bird-symbols-png-logo-0.png" alt=""> </div>
    <div class="text-center mt-4 name"> PassProfile </div>
    <form class="p-3 mt-3" action="/login" method="post">
        <div class="form-field d-flex align-items-center"> <span class="far fa-user"></span> <input type="text" name="username" placeholder="Email"> </div>
        <div class="form-field d-flex align-items-center"> <span class="fas fa-key"></span> <input type="password" name="password" placeholder="Password"> </div>
        <button class="btn mt-3">Login</button>
    </form>
    <div class="text-center fs-6"> <a href="#">Forget password?</a> or <a href="/signup">Sign up</a> </div>
</div>