1. Session Stateless 활용하기
Spring Security에서 담당해야할 부분은 인증과 권한, 이 두 개념과 관련이 있는
Session Stateless라는 개념을 활용해보자
API 서버는 유저의 세션을 관리하는 것이 아닌 특정 토큰에 의해서
요청 Request 헤더에 특정 토큰(주로 Access Token 이라 지칭)을 보내주면 인증이 완료되고
api 기능을 사용할 권한을 갖게 된다
2. Cors(Cross-Origin Resource Sharing) 교차 출처 자원 공유
보통 별도의 API 서버가 존재한다면 앱 어플리케이션인 SPA 프레임워크(react, vue, angular 등)
를 사용하게 될텐데 이때 스프링 프로젝트와는 다른포트를 사용할 것이다 혹은 다른 서버(물리)일 수 있다
이런 경우 출처가 다르다고 판단하여 자원 공유를 정책적으로 금지한게 cors이슈다
이는 스프링 시큐리티에서 특정 도메인을 열어주거나 닫아줄 수 있다
Cors 이슈에 관한 내용이 어떻게 처리되는지 확인해보실 수 있을 것이다
Cors 처리는 여러가지 방법으로 처리가 가능하다
필터를 통한 처리, mvc 설정으로 처리할 수도 있다
이러한 이슈를 처리하는 도중에 브라우저에서 요청을 보내는 preflight에 대해서
스프링 시큐리티에서 어떻게 처리할 수 있는지를 해볼 것이다
3. 프로젝트 생성
IntelliJ
의존성 : Spring Web, Validation(2.3이후 버전일때), Spring Security, Lombok
4. Controller, Service 클래스 생성
@RequiredArgsConstructor
@RequestMapping("/api/v1/test")
@RestController
public class TestController {
private final TestService testService;
@GetMapping("/permit-all")
public Object getTest() throws Exception {
return testService.getTest();
}
@GetMapping("/auth")
public Object getTest2() throws Exception {
return testService.getTest2();
}
}
/permit-all : 스프링 시큐리티에 관계없이 모두 접근 가능
/auth : 스프링 시큐리티 내에서 관리되어야할 URL
5. SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/api/v1/test/permit-all").permitAll()
.antMatchers("/api/v1/test/auth").authenticated()
.antMatchers("/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().disable()
;
}
}
@Configuration
자바로 진행하는 설정 클래스에 붙이는 어노테이션으로 스프링 빈으로 만들고
스프링 프로젝트가 시작될 때 스프링 시큐리티 설정내용에 반영되도록 한다
@EnableWebSecurity
스프링 시큐리티를 활성화하는 어노테이션이다
WebSecurityConfigureAdapter
스프링 시큐리티 설정관련 클래스로 커스텀 설정클래스가 이 클래스의 메소드를 오버라이딩하여 설정하여야
스프링 시큐리티에 반영된다
cors().and()
이 구문은 안에 들어가보면 주석으로 내용이 나와있지만
CorsFilter라는 필터가 존재하는데 이를 활성화 시키는 작업이다
csrf().disable()
세션을 사용하지 않고 JWT 토큰을 활용하여 진행하고 REST API를 만드는 작업이기때문에
csrf 사용은 disable 처리한다
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
현재 스프링 시큐리티에서 세션을 관리하지 않겠다는 뜻 이다
서버에서 관리되는 세션 없이 클라이언트에서 요청하는 헤더에 token을 담아보낸다면
서버에서 토큰을 확인하여 인증하는 방식을 사용할 것이므로 서버에서 관리되어야할 세션이 필요없어지게 된다
authorizeRequests()
이제부터 인증절차에 대한 설정을 진행하겠다는 것이다
antMatchers()
특정 URL 에 대해서 어떻게 인증처리를 할지 결정한다
permitAll()
스프링 시큐리티에서 인증이 되지 않더라도 통과시켜 누구에게나 사용을 열어준다
- authenticated()
요청내에 스프링 시큐리티 컨텍스트 내에서 인증이 완료되어야 api를 사용할 수 있다
인증이 되지 않은 요청은 403(Forbidden)이 내려진다
그리하여 /api/v1/test/permit-all 이라는 url로 들어오는 모든 요청은 200
/api/v1/test/auth 라는 url로 들어오는 요청을 인증을 거친 후, 200/403의 결과가 나올 것이다
6. 인증 구현하기
그 중에서 필요한 인터페이스 두가지가 있다
- UserDetails
- UserDetailsService
UserDetails 는 말그대로 우리가 구현할 User 객체이다
이를 UserDetailsService 에서 구현하여 loadUserByUsername 라는 오버라이딩 메소드에서
Request에서 받은 로그인 데이터를 활용하여 로그인 작업을 해주면 되는 것이다
이때 loadUserByUsername 메소드는 인증된 결과를 가지고 UserDetails 인터페이스를 구현하는
인증대상객체를 리턴해준다
UserDetailsImpl
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
public User getUser() {
return 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() {
UserRoleEnum userRole = user.getRole();
String authority = userRole.getAuthority();
SimpleGrantedAuthority simpleAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleAuthority);
return authorities;
}
}
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));
return new UserDetailsImpl(user);
}
}
일단 기준이 되는 데이터는 User 객체에서 받은 데이터이다
여기에 아이디와 패스워드가 들어있을 것이고 이는 DB에 저장된 값이므로
회원가입 당시에 입력된 데이터일 것이다
이렇게 DB 정보를 SpringSecurity에서 받아오고 난 후 사용자 입력데이터와 비교하는 것이다
loadUserByUsername을 호출하여 인증을 마치고
내부적으로 SecurityUser 클래스와 Password를 passwordEncorder를 이용하여
처리할 수도 있도록 하는 서비스 로직이 존재한다
스프링 시큐리티가 인증을 확인하는 단위는 매 Request 요청이 일어날 때이다
REST API 이기 때문에 매 요청마다 인증이 필요하게 된다
위와 같은 로직을 거친 후 권한 부여를 해주면 된다
HomeController
@Controller
public class HomeController {
private final FolderService folderService;
@Autowired
public HomeController(FolderService folderService) {
this.folderService = folderService;
}
@GetMapping("/")
public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
try {
model.addAttribute("username", userDetails.getUsername());
} catch (NullPointerException e) {
return "redirect:/user/login";
}
if (userDetails.getUser().getRole() == UserRoleEnum.ADMIN) {
model.addAttribute("admin_role", true);
}
List<Folder> folderList = folderService.getFolders(userDetails.getUser());
model.addAttribute("folders", folderList);
return "index";
}
}
'Spring' 카테고리의 다른 글
[Spring] Ioc (Inversion Of Controll), DI (Dependency Injection) (0) | 2022.10.14 |
---|---|
[Spring] @AuthenticationPrincipal 어노테이션 동작 원리 (0) | 2022.10.08 |
[Spring] 서블릿 객체의 생명주기가 무엇인가요? (스프링부트 개념정리 / 인프런 최주호님 강의) (0) | 2022.10.07 |
[Spring] 톰켓이란 무엇인가요? (스프링부트 개념정리 / 인프런 최주호님 강의) (0) | 2022.10.07 |
[Spring] HTTP가 무엇인가요? (스프링부트 개념정리 / 인프런 최주호님 강의) (0) | 2022.10.07 |