BackEnd/Spring

Spring Security

짱호 2020. 7. 12. 17:25
반응형

인증, 인가, 권한, 로그인 등의 기능을 제공해주는 Spring Security를 학습하고 적용해보자.

 

1. Spring Security 개념

스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.

  • 보안과 관련해서 체계적으로 많은 옵션을 제공
  • 일일이 보안 관련 로직을 작성하지 않아도 됨


1. Spring Security 용어

  1. 접근 주체 (Principal)
    • 보호된 리소스에 접근하는 유저
  2. 인증 (Authentication)
    • 현재 유저가 누구인지 확인한다. ex) Form Login
    • 애플리케이션의 작업을 수행할 수 있는 주체임을 확인하는 과정
  3. 인가 (Authorization)
    • 현재 유저가 리소스에 접근할 수 있는 권한이 있는지 검사하는 과정
    • 인증(Authentication) 이후 진행됨
  4. 권한
    • 인증된 주체가 애플리케이션의 리소스를 이용할 수 있는지를 결정하는 요소

2. Spring Security 구조

인증(Authentication)

 

Spring Security의 인증 방식 중 전통적인 세션-쿠키 방식으로 인증을 진행한다.

→ JWT (Json Web Token)은 spring-security-oauth2를 사용함

 

위의 그림에서 번호에 해당되는 인증 과정의 흐름을 간략히 설명하면 다음과 같다.

 

  1. Form 기반 로그인 사용자 인증 요청

  2. AuthenticationFilter가 사용자가 보낸 ID와 PW를 인터셉트하고, 이를 기반으로 Authentication Token(UsernamePasswordAuthenticationToken)을 생성.

  3. AuthenticationManagerAuthentication Token을 전달받음

  4. AuthenticationManager의 구현체인 AuthenticationProvider에서 실제 인증 처리 진행

  5. PasswordEncoder을 통한 패스워드 암호화

  6. UserDetailsService 객체에게 사용자 ID를 넘겨주고, DB에서 사용자 정보(ID, 암호화된 PW, 권한 등)를 UserDetails객체로 전달받음.

  7. AuthenticationProviderUserDetails 객체를 전달 받고, 실제 사용자의 입력정보와 UserDetails에 담긴 정보를 가지고 인증을 시도한다.

8~11. 인증이 완료되면 Authentication 객체를 SecurityContextHolder에 저장

12. 유저에게 session ID와 함께 응답

13. 이후 요청에서 요청 쿠키에서 JSESSIONID을 검증 후 인증 처리

 

위와 같은 인증 과정을 거쳐 접근 주체에 대한 인증을 수행하고, 인가와 권한에 대한 설정을 할 수 있게 된다.


2. Spring Security 프로젝트 구성

1. 의존성 추가

  • Spring Boot & Gradle 기반
implementation 'org.springframework.boot:spring-boot-starter-security'

 

2. Project에 Security Config 추가

 

3. WebSecurityConfigurerAdapter 클래스를 상속받아 SecurityConfig를 구성한다.

@EnableWebSecurity
@Configuration
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private AdminService adminService;

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

    /**
     * 인증 절차를 거치지 않는 목록 정의 ( Security Rule을 무시 )
     * 항상 인가시킬 리소스를 명시
     * resource/static 디렉터리 파일, Swagger API 인가 시킴
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/static/**",
                "/v2/api-docs", "/configuration/ui", "/swagger-resoureces",
                "/configuration/security", "/swagger-ui.html", "/webjars/**", "/swagger/**");
    }

    /**
     * Spring Security Rules
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and() // Session 전략 설정
                .csrf().disable()  // axios post 요청시 csrf token이 없으면 요청이 거절당한다. 일시적으로 사용안함으로 바꿈.
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.NEVER)
            .and()  // 페이지 권한 설정
                .authorizeRequests()
                .antMatchers("/member/licenseInfo", "/admin/login", "/admin/signIn", "/license/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and() // 로그인 설정 
                .formLogin() 
                .loginPage("/admin/login")
                .permitAll()
            .and() // 로그아웃 설정
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/admin/logout"))
                .logoutSuccessUrl("/admin/login")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            .and() // 에러 핸들링
                .exceptionHandling().accessDeniedPage("/admin/error")
            .and() // 기본 auth를 사용
                .httpBasic();
    }

    /**
     * AuthenticationManager 생성
     * Security Login에서 사용 (Swagger)
     * 인증을 위해서 UserDetailsService를 통해 필요 정보를 가져온다.
     * adminService에서 UserDetailsService를 implements하여 loaduserbyUsername() 메서드 구현
     * 비밀번호 암호화
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(adminService).passwordEncoder(passwordEncoder());
    }

    /**
     * AuthenticationManager Bean 등록
     * Custom Login에서 사용
     * UsernamePasswordAuthenticationToken을 파라미터로 인증을 수행
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

 


3. Security를 이용한 Login 구현

3-1. Custom Login

먼저 Front를 구성하여 Login Page를 구현해야 한다. Vue.js를 이용해 Front를 구성하였고 간단한 Login Page를 만들었다. 이제 설정한 Security Rule에 따라 로그인 기능을 구현할 수 있다.

 

1. 아래와 같은 Login Page에서 ID, PW를 입력받는다.

 

 

2. LOGIN 버튼 클릭 시 axios를 통해 Controller Mapping

Controller

/**
     * 로그인
     * @param loginDto - 로그인에 필요한 정보를 담고있는 Dto
     * @param session - 인증된 유저일 때 session 정보를 저장
     * @return - tokenId 발급 -> header를 통해 전달됨
     */
    @PostMapping("/signIn")
    @ApiOperation(value = "로그인", notes = "Spring Security를 통한 로그인 API")
    public ResponseEntity<?> signIn(LoginDto loginDto, @ApiIgnore HttpSession session) {
        String username = loginDto.getUsername();
        String password = loginDto.getPassword();
        
        SignedAdmin admin = adminService.findByAdminId(username);
        
        UsernamePasswordAuthenticationToken token = 
        		new UsernamePasswordAuthenticationToken(username, password);

        Authentication authentication = authenticationManager.authenticate(token);
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
                SecurityContextHolder.getContext());
                
        admin.setTokenId(RequestContextHolder.currentRequestAttributes().getSessionId());
        
        return ResponseEntity.ok(admin);
    }

 

  • Post로 넘어온 ID, PW를 이용해 UsernamePasswordAuthenticationToken을 생성

  • SecurityConfig에서 Bean으로 등록한 authenticationManager를 주입받아 인증을 수행

  • 만약 DB의 유저 정보와 입력받은 정보가 다르다면 오류 발생 → Exception 처리

  • SecurityContextHolder → getContext 메서드를 통해 context를 가져온 후, 인증 정보를 set → SecurityContext에 인증 값 설정됨

  • session에 SecurityContext 값을 넣음

  • UserDetails의 구현체인 admin 객체에 현재 sessionId 값을 TokenId에 set 하고 리턴→ UserDetails을 Custom 하게 구현한 SignedAdmin 객체에 TokenId를 따로 만들어주었다.
  • → TokenId는 추후 Front단(Vue.js)에서 Navigation Guards 구현에도 사용됨

 

AdminService

public SignedAdmin findByAdminId(String adminId) {
        Admin admin = adminRepository.findByAdminId(adminId).orElseThrow(
        		() -> new IllegalArgumentException("아이디 혹은 비밀번호를 확인해주세요."));

        return new SignedAdmin(admin.getAdminId(), admin.getPassword(), 
        					admin.getName(), authorities(admin.getRole()));
}

private Collection<? extends GrantedAuthority> authorities(AdminRole role) {
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role.toString()));
}

 

LoginDto

@Getter // Lombok
@Setter // Lombok
public class LoginDto {
    private String username;
    private String password;
}

 

SignedAdmin

@Getter
@Setter
@RequiredArgsConstructor
public class SignedAdmin implements UserDetails {
    private String adminId;
    @JsonIgnore
    private String password;
    private String name;
    private Collection<? extends GrantedAuthority> role;
    private String tokenId;

    public SignedAdmin(String adminId, String password, String name, 
    			Collection<? extends GrantedAuthority> role) {
                
        this.adminId = adminId;
        this.password = password;
        this.name = name;
        this.role = role;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return role;
    }

    @Override
    public String getPassword() {
        return password;
    }

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

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

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

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

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

 


3-2. Security Login

Swagger API 문서 같은 경우에도 보안이 필요하다. 따라서 Swagger에도 Security를 적용하여 인증 절차를 거친 유저에게만 접속 권한을 준다.

  • Spring Security는 기본적으로 Login Form을 제공하고 있다.
  • 기본 Login Form 이용하여 Swagger에 인증 절차를 구현한다.

 

1. 기본 Login Form

 

 

2. 기본 인증 절차

SecurityConfig

/**
     * AuthenticationManager 생성
     * 기본 Security Login에서 사용 (Swagger)
     * 인증을 위해서 UserDetailsService를 통해 필요 정보를 가져온다.
     * adminService에서 UserDetailsService를 implements하여 loaduserbyUsername() 메서드 구현
     * 비밀번호 암호화
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(adminService).passwordEncoder(passwordEncoder());
    }

 

  • AuthenticationManager를 생성
  • 입력된 ID, PW가 AuthenticationManager로 전달됨
  • 인증 처리를 위해서 UserDetailsService를 통해 정보를 가져옴

 

AdminService

@Override
    public UserDetails loadUserByUsername(String adminId) throws UsernameNotFoundException {
        Admin admin = adminRepository.findByAdminId(adminId)
                .orElseThrow(() -> new UsernameNotFoundException(adminId));

        return new SignedAdmin(admin.getAdminId(), admin.getPassword(), admin.getName(), 
        		authorities(admin.getRole()));
    }

private Collection<? extends GrantedAuthority> authorities(AdminRole role) {
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role.toString()));
    }

AdminService에서 UserDetailsService를 implements 하여, loadUserByusername() 메서드를 구현. 인증에 필요한 정보를 AuthenticationManager로 전달한다.

반응형