티스토리 뷰

1. Spring Security란?

 Spring Security는 스프링 기반 애플리케이션의 보안(인증, 인가 등)을 담당하는 스프링의 하위 프레임워크이다. 만약 이 프레임워크가 존재하지 않았다면 자체적으로 작성해야 할 로직이 많지만, Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해준다는 장점이 있다.

 

* 인증과 인가
인증(Authenticate) : 보호된 리소스에 접근한 대상의 신원 정보를 확인하는 과정(ex. 로그인 과정)
인가(Authorize) : 인증된 사용자가 어떤 리소스에 접근할 수 있고, 어떤 동작을 수행할 수 있는지 검증하는 과정

 

 

2. Spring Security의 특징

  • Servlet API 통합
  • 인증, 인가에 대해 포괄적이고 확장 가능한 지원
  • 필터 기반으로 동작하여 MVC와 분리하여 관리 및 동작
  • annotation을 통한 간단한 설정
  • 세션과 쿠키 방식으로 인증
  • Authentication Manager와 Access Decision Manager를 통해 사용자의 리소스 접근을 관리
  • 인증 관리자는 UserNamePasswordAuthenticationFilter, 접근 관리자는 FilterSecurityInterceptor가 수행

 

3. Spring Security의 기본 구조

  • SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오고 저장하는 일을 담당
  • LogoutFilter : 로그아웃 URL로 지정된 가상 URL에 대한 요청을 감시하고 일치하는 요청이 있으면 사용자를 로그아웃시킴
  • UsernamePasswordAuthenticationFilter : 로그인 URL에 대한 요청을 감시하며, 사용자 인증 처리
  • DefaultLoginPageGeneratingFilter : 로그인 form URL에 대한 요청을 감시하며, 로그인 form 기능을 수행하는데 필요한 HTML 생성
  • BasicAuthenticationFilter : HTTP 기본 인증 헤더를 감시하고 헤더에 Basic 토큰 존재 시 인증 처리
  • RequestCacheAwareFilter : 로그인 성공 후 기존 요청 정보를 재구성하기 위해 사용
  • SecurityContextHolderAwareRequestFilter : HttpServletRequestWrapper를 상속한 SecurityContextHolderAwareRequestWrapper로 HttpServletRequest 정보를 감쌈
  • AnonymoutAuthenticationFilter : 해당 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면, 인증 토큰에 사용자가 익명 사용자로 나타남
  • RequestCacheAwareFilter : 로그인 성공 이후, 기존 요청 정보를 재구성하기 위해 사용
  • ExceptionTranslationFilter : 보호된 요청을 처리할 때 발생할 수 있는 예외를 위임하거나 전달
  • FilterSecurityInterceptor : AccessDecisionManager로써 권한 부여처리를 위임하여 접근 제어를 쉽게 해줌

 

4. Spring Security 등록 및 기본 사용법

4.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'
}

 

4.2 CustomUserDetail 만들기

@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<>(); // 가지고 있는 권한 정보
}
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;
    }
}

 

4.3 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);
    }
}

 

4.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();
    }
}

 

4.5 사용하기

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

 


참고 자료

https://jangjjolkit.tistory.com/24

https://devuna.tistory.com/55

https://maeng0830-note.tistory.com/29