[참고] 초보자가 이해하는 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 |