security 기본

본 내용은 최주호님의 스프링부트 시큐리티 & JWT 강의를 듣고 정리한 내용 입니다.

참고한 깃허브

직접 코드를 수정하고 실행해보면서 테스트 해보는게 이해하기 제일 좋습니다.

기본형태

제일 기본적인 형태를 보려면 Config쪽에 WebMvcConfig 만 남기고 주석처리, controller 에는 아래 / 경로만 설정해준다.

@GetMapping({ "", "/" })
public @ResponseBody String index() {
   return "인덱스 페이지입니다.";
}

시큐리티 의존성을 설치 해주면 처음엔 기본적으로 http://localhost:8080/login으로 이동했을때 로그인 페이지가 생김, application.yml 에 아래처럼 시큐리티 설정을 해주면 해당 계정으로 로그인이 가능하다.

spring:
  security:
    user:
      name: manager
      password: 1234

로그인을 하면 / 경로로 이동한다.

로그인 및 권한설정

/ 경로를 로그인한 유저만 접근가능하고, /admin 경로는 어드민만, /manager 경로는 매니저만 접근 가능하도록 권한설정을 해주는 방법

config 아래 SecurityConfig 를 생성하여 시큐리티 설정을 해준다. 원래는 /login 으로 이동하면 스프링 시큐리티가 낚아 챘는데 설정을 해주면 작동안함

@Configuration // IoC 빈(bean)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션, 
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated() //이주소로 들어오면 인증이 필요하다.
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") //이 권한이 있는 사람만 접근가능
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") //이 권한이 있는 사람만 접근가능
                .anyRequest().permitAll() //저 주소가 아니면 다 권한 허용
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/loginProc") //로그인 주소가 호출이 되면 시큐리티가 낚아채서 로그인을 진행해준다.
                .defaultSuccessUrl("/"); //로그인 성공시 메인페이지로 이동
    }
}
  • @EnableWebSecurity : 스프링 시큐리티 필터가 스프링 필터 체인에 등록이 됨

  • 위와 같은 방식으로 특정 경로에 권한 설정을 해줄수있다. loginPage() 설정으로 인해 권한없는 페이지로 이동시 에러페이지가 뜨는게 아니라 로그인 경로로 이동됨

  • loginProcessingUrl로 인해서 시큐리티가 로그인을 대신 진행해주어 컨트롤러에 따로 만들 필요가 없다.

패스워드

패스워드를 꼭 암호화 해주어야 하기 때문에 SecurityConfig에 아래와 같은 코드도 설정해준다.

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

컨트롤러에서 회원가입시에 아래처럼 암호화해주면 된다.

@PostMapping("/joinProc")
public String joinProc(User user) {
   System.out.println("회원가입 진행 : " + user);
   String rawPassword = user.getPassword();
   String encPassword = bCryptPasswordEncoder.encode(rawPassword);
   user.setPassword(encPassword);
   user.setRole("ROLE_USER");
   userRepository.save(user);
   return "redirect:/";
}

UserDetails

  • 시큐리티가 낚아채서 로그인을 진행 시킬때 로그인이 완료가 되면 시큐리티 session을 만들어낸다. 시큐리티의 세션이 존재함. (Security ContextHolder)

  • 세션에 들어갈수있는 오브젝트 => Authenticaion 타입 객체로 정해져 있다.

  • Authenticaion 안에 User 정보가 있어야 됨

  • User 오브젝트 타입 => UserDetails 타입 객체로 정혀져 있다.

UserDetails 를 구현하는 PrincipalDetails 가 필요하다.

@Data
public class PrincipalDetails implements UserDetails{

	private User user;

	public PrincipalDetails(User user) {
		super();
		this.user = user;
	}
	
	@Override
	public String getPassword() {
		return user.getPassword();
	}

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

	//계정 만료
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	//계정 잠김
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	//계정 기간
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	//계정 활성화 
	@Override
	public boolean isEnabled() {
		return true;
	}

	//해당 유저의 권한을 리턴하는 곳
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> collet = new ArrayList<GrantedAuthority>();
		collet.add(()->{ return user.getRole();});
		return collet;
	}
	
}

getAuthorities

getAuthorities 는 해당 유저의 권한을 리턴하는데 현재 유저의 권한은 role로 스트링타입이다. 근데 타입이 정해져있으니 Collection<GrantedAuthority> 객체를 생성해주어야한다. (ArrayList는 Collection의 자식)

isEnabled

사이트에서 1년동안 로그인 안하면 휴면 계정으로 변경할때, 컬럼에 login할때마다 날짜를 저장하는 컬럼을 두고 그걸로 현재시간과 로그인 시간의 차이가 1년을 초과했을때 리턴은 false로 하면됨

UserDetailsService

  • 로그인시 스프링은 IoC컨테이너에서 UserDetailsService 빈을 찾아 loadUserByUsername을 호출함

  • username 파라미터를 가져옴(클라이언트에서 넘겨준 이름 그대로)

  • UserDetails 로 리턴된 값은 Authentication 내부로 들어감. 그리고 그 객체는 또 세션으로 들어가니 다음과 같은 모양새가 됨. 시큐리티 session(내부 Authentication(내부 UserDetails))

// 시큐리티 설정에서 loginProcessingUrl 걸어놨기 때문에 로그인 요청이 오면 자동으로
// UserDetailsService 타입으로 IoC되어있는 loadUserByUsername 함수가 실행됨 (규칙임)
@Service
public class PrincipalDetailsService implements UserDetailsService{

   @Autowired
   private UserRepository userRepository;

   //알아서 다해줌 
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      User user = userRepository.findByUsername(username);
      if(user == null) {
         return null;
      }
      return new PrincipalDetails(user);
   }

}

특정주소에 직접 권한걸기

SecurityConfig 에 아래와같은 어노테이션을 걸어주면 특정어노테이션이 활성화가 된다.

securedEnabled는 @Secured 를 활성화 하고

prePostEnabled는 @PreAuthorize 와 @PostAuthorize 를 활성화 시킴

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter{

그리고 아래와 같이 사용한다.

//권한 하나만 걸고싶으면 secured
@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info() {
   return "개인정보";
}

//권한 여러개를 걸고싶으면 preAuthorize
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") //data메서드 실행되기 전 실행
@PostAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") //data메서드 실행후에 실행됨, 잘 안씀
@GetMapping("/info")
public @ResponseBody String data() {
   return "데이터 정보";
}

websecurityconfigureradapter deprecated 문제

이 문제로 인해 SecurityConfig를 다르게 변경해주어야한다.

@Configuration
@EnableWebSecurity // 시큐리티 활성화 -> 기본 스프링 필터체인에 등록
public class SecurityConfig {

   @Autowired
   private UserRepository userRepository;

   @Autowired
   private CorsConfig corsConfig;

   @Bean
   SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      return http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .formLogin().disable()
            .httpBasic().disable() //기본인증방식 - 보안에 안좋음
            .apply(new MyCustomDsl()) // 커스텀 필터 등록
            .and()
            .authorizeRequests(authroize -> authroize.antMatchers("/api/v1/user/**")
                  .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                  .antMatchers("/api/v1/manager/**")
                  .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                  .antMatchers("/api/v1/admin/**")
                  .access("hasRole('ROLE_ADMIN')")
                  .anyRequest().permitAll())
            .build();
   }

   // corsConfig : @CrossOrigin 같은 어노테이션을 걸어주는건 인증이 없을때, 인증이 있을땐 필터에 아래처럼 등록해줘야한다.
   public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
      @Override
      public void configure(HttpSecurity http) throws Exception {
         AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
         http
               .addFilter(corsConfig.corsFilter())
               .addFilter(new JwtAuthenticationFilter(authenticationManager))
               .addFilter(new JwtAuthorizationFilter(authenticationManager, userRepository));
      }
   }

}j

Last updated