[참고] 초보자가 이해하는 Spring Security
* 스프링 시큐리티가 애플리케이션 보안을 구성하는 두가지 영역
- 인증(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)를 반환한다.
=> 반환하기 직전에 패스워드를 검증하는 것!
'ICT Intern > Spring Security' 카테고리의 다른 글
| [Spring] Spring Security 에러 페이지 커스터마이징 (0) | 2019.04.11 | 
|---|---|
| [Spring] Spring Security 메세지 커스터마이징 (0) | 2019.04.11 | 
| [Spring] Thymeleaf 알아보기 (0) | 2019.04.09 | 
| [Spring] Spring Security 로그인 커스터마이징 (0) | 2019.04.09 | 
| [Spring] Spring Security 암호화 (0) | 2019.04.09 |