끄적끄적 코딩일지

[Spring 기초] Spring Security 사용하기 본문

Spring

[Spring 기초] Spring Security 사용하기

BaekGyuHyeon 2022. 6. 4. 19:21

Spring Security란?

Spring Web에서 사용자에 대한 인증과 권한등을 관리하고 주요 공격으로부터 어플리케이션을 보호하는 프레임워크이다. 명령형과 반응형 어플리케이션 모두에서 작동 가능하기때문에 Spring 기반 프로그램의 표준 보안 프레임워크로 사용된다.


Spring Security 구조

1. 인증하기

사용자 인증을 하고 해당 정보를 저장하거나 권한을 검사하는등의 역할을 수행하는 아키텍처는 다음과 같다.

 1. Client에서 로그인 요청(Id, Password 정보 전송)

 2. AuthenticationFilter에서 받은 정보를 AuthenticationManager에게 요청

 3. AuthenticationManager에서는 AuthenticationProvider에게 요청을 하여 사용자 정보를 Database에서 찾고 해당 사용자 정보를 토대로 인증정보와 Session을 생성한다.

 4. 이후 인증정보는 Session저장소인 SecurityContextHolder에 저장한다.

 5. Client에게 Sesison ID와 함께 응답을 보낸다.

 6. 이후 요청에서는 요청쿠키의 JSEESSIONID값으로 검증을 하여 유요한 인증인경우 ecurityContextHolder에 저장되어있는 Authentication을 제공해준다.

 

2. Filter

Spring Security는 Client요청을 Controller와 연결해주기 전에 여러 Filter을 거치며 요청을 검사하며 반대로 Client에게 보내는 Response또한 다시 Filter을 거치며 인증을 진행한다. 또한 CustomFilter을 추가하거나 Filter를 재정의함으로써 Jwt나 OAuth등의 인증방식을 구현할 수 있다.

Filter의 종류는 다음과 같다.

 1. SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할

 2. Logout Filter : 설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리(인증정보 삭제)

 3. AuthenticationFo;ter : 설정된 로그인 URL으로 오는 요청을 감시하며, 유저 인증 처리(보통 아이디, 비밀번호 기반의 UsernamePasswordAuthenticationFilter 사용)

 4. DefaultLoginPageGeneratingFilter : 인증을 위한 로그인 페이지 Url을 감시

 5. BasicAuthenticationFilter : HTTP 해더에 있는 Authentication 값을 감시

 6. RequestCacheAwareFilter : 로그인 성공 후 원래 요청 정보를 재구성 하기 위해 사용.

 7. SecurityContextHolderAwareRequestFilter : HttpServletReques 전후로 다음 필터에게 필요한 부가 정보 제공

 8. AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 인증정보가 되지 않았다면 익명 사용자 토큰 발급

 9. SessionManagementFilter : Session에 인증정보를 저장하거나 조회하는 행동감시. 즉 인증된 사용자의 모든 요청을 감시한다.

 10. ExceptionTranslationFilter :  인증이 필요한 요청중 발생하는 예외를 처리하거나 전달하는 필터

 11. FilterSecurityInterceptor : 접근제어결정을 재정의할수 있는 필터


사용하기

Spring Security를 그냥 사용만 하자면 위의 내용을 전부 알아야 할 필요는 없다. 실제로 위의 부분을 자동으로 처리해주는 부분이 존재하고 Spring Security의 기본적인것 외에 필요한 부분만 재정의하여 사용하면 된다. 이 글에서는 가장 기본적인 ID, Password 기반의 인증을 처리하는 방법을 소계하려고 한다.

Step 1. 라이브러리 추가

Maven 사용 - pom.xml 수정

...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
....

Gradle 사용 - build.gradle 수정

dependencies{
	...
	implementation 'org.springframework.boot:spring-boot-starter-security'
}

버전을 지정안하면 가장 최신의 버전 라이브러리를 가져온다. 글 작성 시점의 가장 최신버전은 2.7.0 버전으로 향후 업데이트에 따라 사용법이 달라질 수도 있다.


Step 2. CustomUserDetail 만들기

UserDetail은 인증정보를 만드는데 필요한 사용자 정보를 제공해 준다.

만약 아래와 같은 사용자 정보 Entity를 사용한다고 하자.

@Entity
class Member{
    @Id
    @GenerateValue(stragy=StragyType.IDENTITY)
    private long id;
    
    @Column
    private String username; // 로그인시 받는 id값
    
    @Column
    private String password; // 로그인시 받는 password값
    
    @ElementCollection
    @CollectionTalbe(name="roles",joinColumns=@JoinColumn(name="member_id"))
    @Column(name="role")
    private List<String> hasRole = new ArrayList<>(); // 가지고 있는 권한 정보
}

 사용자 정보를 입력할때 어떤 변수가 Username 인지, 어떤 변수가 password인지 , 어떤 변수가 권한정보를 나타내는지 알아야 한다.

 

class MemberDetail implements UserDetails {
    private Member member;

    public MemberDetail(Member member) {
        this.member = member;
    }
	// 권한정보 제공
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 특별한 권한 시스템을 사용하지 않을경우
        // return Collections.EMPTY_LIST;
        // 를 사용하면 된다.
        ArrayList<GrantedAuthority> auths = new ArrayList<>();
        for(String role : member.getHasRole()){
            auths.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return role;
                }
            });
        }
        return auths;
    }
	// 비밀번호 정보 제공
    @Override
    public String getPassword() {
        return member.getLoginPw();
    }
	// ID 정보 제공
    @Override
    public String getUsername() {
        return member.getLoginId();
    }
	// 계정 만료여부 제공
    // 특별히 사용을 안할시 항상 true를 반환하도록 처리
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 계정 비활성화 여부 제공
    // 특별히 사용 안할시 항상 true를 반환하도록 처리
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 계정 인증 정보를 항상 저장할지에 대한 여부
    // true 처리할시 모든 인증정보를 만료시키지 않기에 주의해야한다.
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }
    // 계정의 활성화 여부
    // 딱히 사용안할시 항상 true를 반환하도록 처리
    @Override
    public boolean isEnabled() {
        return true;
    }
}

Step 3. CustomUserDetailService 만들기

사용자의 인증정보를 제공할 UserDetail을 만들었다면 받은 정보를 토대로 사용자를 찾고 UserDetail을 만들어서 SecurityContextHoder에 제공할 CustomUserDetailService를 만들면 된다.

 

@Service
@RequriedArgsConstructor
public class MemberDetailService implements UserDetailService{
    
    private final MemberRepo repo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username으로 Repository에서 Member을 찾는 method 
        Member member = repo.findByUsername(username);
        if(member == null)
            throw new UsernameNotFoundException("계정을 찾을 수 없습니다.");
        return new MemberDetail(member);
    }
}

위에서 비밀번호는 사용안하냐고 물을수 있다.

보통 Database에 비밀번호를 암호화 해서 저장하므로 Database에서 해당 정보를 찾을때 ID값과 암호화된 비밀번호를 비교해가면서 찾는것보다 먼저 ID값으로 먼저 찾고 비밀번호를 복호화해서 비교하는게 더 빠르고 정확하다.

때문에 대부분 회원가입할때 ID 중복을 확인하는 이유가 ID값으로 찾았을때 여러개의 계정정보가 검색되면 어떤 계정으로 인증을 해야할지 알수없기 때문.

 

Step 4. Configuration 만들기

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter{

    private final MemberDetailService detailService;
    
    /**
    * Spring Security의 앞단 설정을 할수 있다.
    * debug, firewall, ignore등의 설정이 가능
    * 단 여기서 resource에 대한 모든 접근을 허용하는 설정할수도 있는데
    * 그럴경우 SpringSecuity에서 접근을 통제하는 설정은 무시해버린다.
    */
      @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/h2-console/**");
    }
    
    /**
    * Spring Security 기능 설정을 할수 있다.
    * 특정 리소스에 접근하지 못하게 하거나 반대로 로그인, 회원가입 페이지외에 인증정보가 있어야
    * 접근할 수 있도록 설정할 수 있다.
    * 특정 리소스의 접근허용 또는 특정 권한 요구,로그인, 로그아웃, 로그인,로그아웃 성공시 Event
    * 등의 설정이 가능하다.
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        
        // 요청에 대한 설정
        // permitAll시 해당 url에 대한 인증정보를 요구하지 않는다.
        // authenticated시 해당 url에는 인증 정보를 요구한다.(로그인 필요)
        // hasAnyRole시 해당 url에는 특정 권한 정보를 요구한다.
        // resources에 대해 접근혀용을 해야지 브라우저에서 로그인없이 js파일이나 css파일에 접근할 수 있다.
        http
                .authorizeRequests() // 요청에 대한 설정
                .antMatchers("/notice/**").permitAll() 
                .antMatchers("/main").permitAll()
                //.antMatchers("/js/**").permitAll()
                //.antMatchers("/css/**").permitAll()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/admin/**").hasAnyRole("ADMIN") 
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/member/login")
                .defaultSuccessUrl("/main",true)
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/member/logout")
                .logoutSuccessUrl("/login")
                .logoutSuccessHandler(logoutHnadler).permitAll();
    }
    
    /**
    * 사용자 인증 관련 설정
    * Custom User Detail Service를 지정하고 PasswordEncoder을 사용해서 비밀번호를 암호화 할 수 있다.
    * 참고로 비밀번호는 같은 암호화방식을 사용해서 Database에 저장해야지 인증가능하다.
    */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(detailService).passwordEncoder(getPasswordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

formLogin을 사용하여 login을 진행할 url을 지정하거나 login성공시 이동 url, loginpage를 지정하거나

logout을 사용하여 로그아웃 설정을 할 수 있다.

loginProcessingUrl이나 logoutUrl은 따로 Controller에 정의할 필요 없고 request body에

{"username":"id값","password":"password 값"}

형태로 넣어서 해당 url에 Post요청을 하면 자동으로 인증을 진행한다. (logout은 GET)물론 usernameParameter, passwordParameter을 사용해서 특정값을 Id, Password으로 인식하게 할 수 있다.(따로 설정을 안하면 기본적으로 id는 username, 비밀번호는 password의 값을 인식한다.)

 

▶︎ csrf란 사이트간 요청을 위조하여 공격하는 방식이다. 위에서 해당 기능을 비활성화 하는 이유는 Spring의 공식문서상에서 브라우저 없이 클라이언트에서 사용하는 API 서비스만 개발하는 경우는 Jwt나 AOuth등의 별도의 인증 방식을 사용하여 굳이 필요가 없다는 것이다. 

► 위에서 csrf 보호를 비활성화 한 이유는 csrf 공격은 데이터 변조가 가능한 POST,PUT등의 GET 이외의 Method에 대해서 별도의 인증을 요구하기 때문에 해당 설정도 따로 해주어야 하기 때문이다.(즉 따로 설정을 안하면 일반적으로 해당 Method를 사용하지 못한다. 때문에 배우는 단계에서는 굳이 사용할 필요가 없다.)

 

Step 5. 사용하기

여기까지 했으면 Spring Security에서 자동으로 특정 url에 대한 인증요구나 로그인 , 로그아웃등을 자동으로 spring security에서 진행한다. 또한 Controller에서 @AuthenticationPrincipal 를 사용해서 인증정보를 얻어올 수 있다. (UserDetail)

 

@Controller
public class PageController{
    @GetMapping("/member")
    public Member getLoginMemberInfo(@AuthenticationPrincipal MemberDetail detail){
        return detail.getMember();
    }
}

 


끝으로...

현제 WebSecurityConfigurerAdapter사용없이 Bean 등록만으로 Spring Security 설정이 가능해지면서

WebSecurityConfigurerAdapter가 deprecated되었다. 따라서 앞으로는 WebSecurityConfigurerAdapter가 변경되거나 사라질수 있으니 이 글에서 다루는 SpringSecurity의 버전과 최신버전을 확인하길 바란다.