ICT Intern/Spring Security

[Spring] Spring Security 기본 개념 정리

칸타탓 2019. 4. 10. 14:13

[참고] 초보자가 이해하는 Spring Security

https://postitforhooney.tistory.com/entry/SpringSecurity-%EC%B4%88%EB%B3%B4%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-Spring-Security-%ED%8D%BC%EC%98%B4

 

* 스프링 시큐리티가 애플리케이션 보안을 구성하는 두가지 영역

- 인증(Authentication)

애플리케이션의 작업을 수행할 수 있는 주체(사용자)인 것

- 권한(Authorization)

인증된 주체가 애플리케이션의 동작을 수행할 수 있도록 허락되있는지를 결정하는 것

=> 권한 승인이 필요한 부분으로 접근하려면 인증 과정을 통해 주체가 증명 되어야만 한다.

 

* 스프링 시큐리티의 권한 부여

- 웹 요청 권한

- 메소드 호출 및 도메인 인스턴스에 대한 접근 권한

 

* HTTP 기본 인증(HTTP Basic Authentication) 매커니즘

ex) 로그인 화면을 통해서 아이디와 비밀번호를 입력받아 로그인하는 과정 (폼 기반 로그인)

 

* 프로젝트에서 시작하기

- Gradle에서 사용할 경우

dependencies {
  compile 'org.springframework.security:spring-security-web:4.2.2.RELEASE'
  compile 'org.springframework.security:spring-security-config:4.2.2.RELEASE'
}

- Maven에서 사용할 경우

<dependencies>
  <dependency>
  	<groupId>org.springframework.security</groupId>
  	<artifactId>spring-security-web</artifactId>
  	<version>4.2.2.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
  	<artifactId>spring-security-config</artifactId>
  	<version>4.2.2.RELEASE</version>
  </dependency>
</dependencies>

 

* Java Configuration

WebSecurityConfigurerAdapter를 상속받은 클래스에 @EnableWebSecurity 어노테이션을 명시

=> springSecurityFilterChain가 자동으로 포함되어진다.

 

springSecurityFilterChain을 등록하기 위해서는 AbstractSecurityWebApplicationInitializer를 상속받는 클래스를 만든다.

=> XML을 사용하는 것보다 이렇게 자바 기반으로 구성하는 것이 더욱 쉬움

 

아래와 같이 추가해주면 기본 적용 완료!

public class ApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
  @Override
  protected Class<?>[] getRootConfigClasses() { 
    return new Class[] { 
    	WebSecurityConfig.class
    };
  }
  // ... other overrides ...
}

 

* HTTP Security

configure(HttpSecurity http) 메소드를 통해서 자신만의 인증 매커니즘 구성

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.httpBasic()
		.and()
		.authorizeRequests() //요청에 대한 권한을 지정
	.antMatchers("/users/{userId}").access("@authenticationCheckHandler.checkUserId(authentication,#userId)") .antMatchers("/admin/db/**").access("hasRole('ADMIN_MASTER') or hasRole('ADMIN') and hasRole('DBA')")
	.antMatchers("/register/**").hasRole("ANONYMOUS")
	.and()
	.formLogin() //폼을 통한 로그인을 이용한다는 의미
		.loginPage("/login") ///login 경로로 제공
		.usernameParameter("email")
		.passwordParameter("password")
		.successHandler(successHandler())
		.failureHandler(failureHandler())
		.permitAll();
}

 

* antMatchers() 다음으로 지정할 수 있는 항목

  • anonymous()

    인증되지 않은 사용자가 접근할 수 있다.

  • authenticated()

    인증된 사용자만 접근할 수 있다.

  • fullyAuthenticated()

    완전히 인증된 사용자만 접근할 수 있다.

  • hasRole() or hasAnyRole() - 역할 (ROLE으로 표현)

  • hasAuthority() or hasAnyAuthority() - 권한 (ROLE_ADMIN)

    특정 권한을 가지는 사용자만 접근할 수 있다.

  • hasIpAddress()

    특정 아이피 주소를 가지는 사용자만 접근할 수 있다.

  • access()

    SpEL 표현식에 의한 결과에 따라 접근할 수 있다.

  • not() 접근 제한 기능을 해제

  • permitAll() or denyAll()

    접근을 전부 허용하거나 제한

  • rememberMe()

    리멤버 기능을 통해 로그인한 사용자만 접근할 수 있다.

 


 

* AuthenticationManagerBuilder

- 인증 객체를 만들 수 있도록 제공

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
    	.inMemoryAuthentication().withUser("scott").password("tiger").roles("ROLE_USER");
}

 

*  스프링 시큐리티 3.0부터 표현 기반의 어노테이션을 사용할 수 있다.

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
	// ...
}
public interface BankService {
  @PreAuthorize("isAnonymous()")
  public Account readAccount(Long id);

  @PreAuthorize("isAnonymous()")
  public Account[] findAccounts();

  @PreAuthorize("hasAuthority('ROLE_TELLER')") 
  ublic Account post(Account account, double amount);
}

 

* Remember-Me

단순히 아이디를 기억하는 것이 아닌, 로그인 정보를 유지하는 것

TokenRepository 인터페이스를 구현한다.

- 이해 안 감 ㅠㅠ 나중에 다시보기!

@Transactional
public class TokenRepositoryImpl implements PersistentTokenRepository {
	@Autowired private TokenRepository tokenRepository;

	@Override
	public void createNewToken(PersistentRememberMeToken token) {
		Token newToken = new Token();
		newToken.setEmail(token.getUsername());
		newToken.setToken(token.getTokenValue());
		newToken.setLast_used(token.getDate());
		newToken.setSeries(token.getSeries());
		tokenRepository.save(newToken);
	}

	@Override
	public void updateToken(String series, String tokenValue, Date lastUsed) {
		Token updateToken = tokenRepository.findOne(series);
		updateToken.setToken(tokenValue);
		updateToken.setLast_used(lastUsed);
		updateToken.setSeries(series);
		tokenRepository.save(updateToken);
	}

	@Override
	public PersistentRememberMeToken getTokenForSeries(String series) {
		Token token = tokenRepository.findOne(series);
		PersistentRememberMeToken persistentRememberMeToken = new PersistentRememberMeToken(token.getEmail(), series,
				token.getToken(), token.getLast_used());
		return persistentRememberMeToken;
	}

	@Override
	public void removeUserTokens(String username) {
		tokenRepository.deleteByEmail(username);
	}
}

 

* Password Encoding

AuthenticationManagerBuilder.userDetailsService().passwordEncoder()를 통해 패스워드 암호화에 사용될 PasswordEncoder 구현체를 지정할 수 있다.

 

@Bean으로 등록해두는 이유는, 저장된 password를 비교할 수 있기 때문에!

password는 PasswordEncoder에 의해 암호화되어 저장된다.

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder(){
	return new BCryptPasswordEncoder();
}
if(!passwordEncoder.matches(updateUser.getPassword(), currentUser.getPassword())){
	throw new RuntimeException("Not password equals...");
} //이렇게 비교할 수 있다.

 

* WebSecurity Ignoring

보안이 적용되지 않도록 할 수 있도록 지원

@Override
    public void configure(WebSecurity web) throws Exception {
	web
          .ignoring()
          .antMatchers("/resources/**","/webjars/**");
}

 

* Localization

메시지에 대한 현지화를 지원한다.

메시지 소스 관련 프로퍼티 파일들은 spring-security-core.jar에 포함 되어있다. 메시지 프로퍼티 파일들을 메시지 소스로 등록하면 된다.

 

* AuthenticationSuccessHandler & AuthenticationFailureHandler

public class AuthenticationSuccessHandlerImpl extends SavedRequestAwareAuthenticationSuccessHandler {
	private static final Logger logger = LoggerFactory.getLogger(AuthenticationSuccessHandlerImpl.class);

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		logger.info("Login Success... - {}", authentication.getPrincipal());
		response.sendRedirect("/?login");
	}
}

public class AuthenticationFailureHandlerImpl extends SimpleUrlAuthenticationFailureHandler {
	private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureHandlerImpl.class);

	public AuthenticationFailureHandlerImpl() {
		this.setDefaultFailureUrl("/login?error");
	}

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		logger.info("Login Failed... - {}", request.getParameter("email"));
		super.onAuthenticationFailure(request, response, exception);
	}
}

로그인을 성공했을때 호출(인증 객체가 생성되어진 후)되기 때문에 Authentication 인스턴스 파라미터를 이용할 수 있다.

로그인 실패 시 SimpleUrlAuthenticationFailureHandler의 defaultFailureUrl를 지정하면 SPRING_SECURITY_LAST_EXCEPTION에 대한 정보를 가지면서 해당 경로로 이동하게 된다.

 

* 시큐리티 커스터마이징하기

- 제공되는 기본 로그인 페이지 대신 커스터마이징 로그인 페이지를 만들기

- 따로 구현해보기

 

* UserDetails => 부가 정보를 위해

스프링 시큐리티는 사용자 정보를 UserDetails 구현체로 사용한다.

=> org.springframework.security.core.userdetails.User라는 클래스를 제공

이름과 패스워드 그리고 권한들에 대한 필드만 존재하기 때문에 이메일 정보 또는 프로필 이미지 경로 등과 같은 부가적인 정보를 담을 수 없다.

 

따라서 UserDetails 구현체가 필요하다.

직접 만들거나 org.springframework.security.core.userdetails.User를 상속받는다.

public class UserDetails extends User {
	private static final long serialVersionUID = -4855890427225819382L;
    
	private Long id;
	private String nickname;
	private String email;
	private Date createdAt;

	//생성자
	public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}

	public UserDetails(User user) {
		super(user.getEmail(), user.getPassword(), user.isAccountNonExpired(), user.isAccountNonLocked(),
				user.isCredentialsNonExpired(), user.isEnabled(), authorities(user));
		this.id = user.getId();
		this.nickname = user.getNickname();
		this.email = user.getEmail();
		this.createdAt = user.getCreatedAt();
	}

	//getter, setter
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		this.nickname = nickname;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public Date getCreatedAt() {
		return createdAt;
	}

	public void setCreatedAt(Date createdAt) {
		this.createdAt = createdAt;
	}

	private static Collection<? extends GrantedAuthority> authorities(User user) {
		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
		user.getAuthorities().forEach(a -> {
			authorities.add(new SimpleGrantedAuthority(a.getAuthority()));
		});
		return authorities;
	}

	public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities,
			String nickname) {
		super(username, password, authorities);
		this.nickname = nickname;
		this.email = username;
	}

	public UserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
}

 

* UserDetailsService

org.springframework.security.core.userdetails.UserDetailsService 구현체는 스프링 시큐리티 인증 시에 사용된다.

UserRepository를 통해 저장된 인증정보를 검색한 후 존재하지 않다면 UsernameNotFoundException 반환, 있다면 UserDetails 객체를 반환

@Service
public class UserDetailsService implements UserDetailsService {
  @Autowired private UserRepository userRepository;
  
  @Override
  public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    User user = userRepository.findByEmail(email); 
    if(user == null){
    	throw new UsernameNotFoundException(email);
    }
    UserDetails userDetails = new com.kdev.app.security.userdetails.UserDetails(user);
    return userDetails;
  }
}

 

* AuthenticationProvider

패스워드 검증은 AuthenticationProvider 구현체에서 진행한다.

AuthenticationProvider 구현체에서는 authenticate() 메소드를 통해서 Authentication 객체(UsernamePasswordAuthentication)를 반환한다.

=> 반환하기 직전에 패스워드를 검증하는 것!