Spring Security 란?
Spring 기반의 애플리케이션의 보안을 담당하는 하위 프레임워크이다. 또한 인증 과 권한에 대한 부분을 Filter 흐름에 따라 처리 하고 있다. 해당 구조는 아래에 설명을 하겠다.
Spring Security 구조
위와 같이
client 에서 login 을 요청하면 filter 에서 먼저 처리를 한 뒤 AuthenticationManager 에서 토큰 객체생성을 하고 Provider 에게 준다. 그 뒤 UserDetailsService 에 인증처리를 요청하고 다시 맞으면 주소에 대한 접근을 허용해준다.
예제코드를 보면 좀더 이해가 될것이다.
Spring Security 예제
먼저 Spring 이 Spring Security 5.7.0-M2 버전부터 바뀌게 되었다. 원래는 상속을 받던것을 @Bean 으로 관리 할수 있도록 사용이 된다.
package com.chatroom.chatroom.config;
@Configuration
public class WebSecurityConfig {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf ->
csrf.disable()
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/errer").permitAll()
.requestMatchers("/").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(login -> login
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/")
.permitAll()
)
.logout((logout) -> logout
.logoutSuccessUrl("/login")
.permitAll()
)
.exceptionHandling(ex -> ex
.accessDeniedPage("/errer"))
.sessionManagement(session -> session
.maximumSessions(1) //세션 최대 허용 수
.maxSessionsPreventsLogin(false) //중복 로그인
);
return http.build();
}
}
위의 코드를 보면, csrf 를 불가능하게 만들었고 해당 csrf 는 다음에 게시글을 작성하겠습니다.
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/errer").permitAll()
.requestMatchers("/").hasRole("USER")
.anyRequest().authenticated())
이 매서드는 SecurityFilterChain 반환해준다. 즉, 처음 구조에 대해 이야기했던 첫번째로 받게 되는 녀셕인것이다.
requestMatchers("/login", "/errer").permitAll() 는 /login, /errer 는 인증을 안해도 접근이 가능하게 만들었다.
.requestMatchers("/").hasRole("USER") 인증이 성공하여 권한이 USER 라면 / 주소에 대한 접근을 허용했다.
마지막으로 anyRequest().authenticated() 는 모든 경로의 접근을 막아놨다.
.formLogin(login -> login
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/") .permitAll())
.loginPage("/login") 해당 로그인 페이지는 Controller 에서 /login Mapping 해서 return 해주는 login.html 페이지에서
.username, password 파라미터를 받는것이다. 만약 로그인 성공하게 된다면 / 주소로 이동하게 된다.
.exceptionHandling(ex -> ex
.accessDeniedPage("/errer"))
.sessionManagement(session -> session
.maximumSessions(1) //세션 최대 허용 수
.maxSessionsPreventsLogin(false) //중복 로그인);
exceptionHandling 는 만약 에러가 발생된다면 에러 페이지를 보여주는것이다. 즉, Controller 에서 /errer Mapping 해서 return 해주는 errer.html 페이지를 보내는것이다.
sessionManagement 는 위에 설명된것 처럼 중복 로그인과 세션 최대 허용 수를 설정하였다.
UserDetailsService 예제
위의 구조에 설명한것처럼 인증을 요청하여 받아서 이 유저의 username, password 가 맞는지 확인 하는 작업이다.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserEntityRepository userEntityRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//인증구현
System.out.println("?! : "+username);
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserEntity entity = userEntityRepository.findById(username)
.orElseThrow(() ->
new UsernameNotFoundException("찾을수 없음. : "+username));
UserDetail user = new UserDetail(entity);
return user;
}
}
위에 코드 처럼 username 를 가져와 DB 에 있는 user 를 가져올때 정보가 없으면 오류로 던지게 되고, 있다면 UserDetail 를 만들어서 다시 반환하게 된다.
아래에 UserDetail 코드를 첨부하였다.
UserDetail.java
package com.chatroom.chatroom.domain;
import com.chatroom.chatroom.domain.dto.UserDTO;
import com.chatroom.chatroom.domain.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class UserDetail implements UserDetails {
private UserDTO user;
public UserDetail(UserDTO user) {
this.user = user;
}
public UserDetail(UserEntity entity){
user = user.from(entity);
}
@Override
public String getPassword() {
return user.user_password();
}
@Override
public String getUsername() {
return user.user_password();
}
//계정이 만료되지 않았는지 리턴 (true: 만료안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
//계정이 잠겨있는지 않았는지 리턴. (true:잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
//비밀번호가 마료되지 않았는지 리턴한다. (true:만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정이 활성화(사용가능)인지 리턴 (true:활성화)
@Override
public boolean isEnabled() {
return true;
}
//계정이 갖고있는 권한 목록은 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(() -> {
return "ROLE_USER";
});
return collectors;
}
}
SecurityContextHolder ?
인증된 사용자 정보를 관리 해주는 녀셕이다.
정확하게는 Principal 를 Authentication 에서 관리하고,
Authentication 를 SecurityContext 가 관리하고,
SecurityContext 는 SecurityContextHolder 가 관리하게 된다.
로그인한 정보들을 가져올수 있다.
UserDetail test = (UserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(test.getAuthorities());
System.out.println(test.getPassword());
주의사항
Spring Security 는 기본적으로 비밀번호를 PasswordEncoder 를 통해 저장하게 되야 된다.
만약 기본 데이터의 비밀번호를 test 라고 저장했다면 {noop}test 라고 데이터를 만들어 놓자.
'Spring' 카테고리의 다른 글
Lombok (0) | 2023.05.27 |
---|---|
WebSocket(2) (0) | 2023.05.25 |
Websocket (1) (0) | 2023.05.21 |
thymeleaf (0) | 2023.05.20 |
Querydsl (2) | 2023.05.19 |