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";
    }
}