쌓고 쌓다

Spring Security JWT를 위한 로그인 본문

프로그래밍/spring

Spring Security JWT를 위한 로그인

승민아 2024. 1. 18. 13:08

시큐리티 세션 = Authentication ( UserDetails ) 구조이다.

 

우리가 저장할 정보는 UserDeatils 객체여야하며 그것은 Authentication으로 감싸져있다.

이것을 세션으로 사용한다.

 

JWT를 사용하면 세션을 만들 이유는 없지만. 권한처리 때문에 일단 사용한다고하는데

JWT에 권한도 담겨져 있는것이 아닌가 생각이 들지만 나중에 JWT를 완성해보면 이해가 될것같다.

 

회원의 정보 TestUser는 다음과 같이 설계했다.

 

TestUser를 UserDetails로 만들자.

 

PrincipalDetails (UserDetails)

package com.example.spotserver.config.auth;

import com.example.spotserver.securityStudy.TestUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class PrincipalDetails implements UserDetails {

    private TestUser testUser;

    public PrincipalDetails(TestUser testUser) {
        this.testUser = testUser;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return testUser.getRole();
            }
        });
        return collection;
    }

    @Override
    public String getPassword() {
        return testUser.getPassword();
    }

    @Override
    public String getUsername() {
        return testUser.getName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }


    public TestUser getTestUser() {
        return testUser;
    }
}

계정에 대한 정책은 없기에 모두 return true; 해주었다.

 

PrincipalDetailsService.java

package com.example.spotserver.config.auth;

import com.example.spotserver.securityStudy.TestUser;
import com.example.spotserver.securityStudy.TestUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;


@Service
public class PrincipalDetailsService implements UserDetailsService {

    private TestUserRepository testUserRepository;

    @Autowired
    public PrincipalDetailsService(TestUserRepository testUserRepository) {
        this.testUserRepository = testUserRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        TestUser testUser = testUserRepository.findByName(username);
        return new PrincipalDetails(testUser);
    }
}

 

CorsConfig.java

package com.example.spotserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig  {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); // 내 서버가 응답할때 json을 자바스크립트에서 처리할 수 있게 할지를 설정하는 것 (ajax와 같은걸로 요청했을때 응답을 처리할 수 있게 해줄까말까)
        config.addAllowedOrigin("*"); // 모든 IP에 응답 허용
        config.addAllowedHeader("*"); // 모든 헤더에 응답 허용
        config.addAllowedMethod("*"); // 모든 post, get, put, delete 등등 요청을 허용하겠다.
        source.registerCorsConfiguration("/**", config);

        CorsFilter corsFilter = new CorsFilter(source);
        return corsFilter;
    }
}

메서드에 @Bean 어노테이션을 붙였기에 반환되는 CorsFilter는 IoC에 빈으로 등록된다.

CorsConfig.corsFilter로 반환되는 객체는 항상 동일한 객체가 된다.

 

SecurityConfig.java

package com.example.spotserver.config;

import com.example.spotserver.config.jwt.JwtAuthenticationFilter;
import com.example.spotserver.filter.MyFilter3;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private CorsConfig corsConfig;


    // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public SecurityConfig(CorsConfig corsConfig) {
        this.corsConfig = corsConfig;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.addFilterBefore(new MyFilter3(), SecurityContextPersistenceFilter.class);
        http.csrf(AbstractHttpConfigurer::disable);
        http.sessionManagement(session ->
                session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 세션 생성 X

        http.authorizeHttpRequests(request ->
                request
                        .requestMatchers("/user/**").authenticated() // 인증만 된다면 들어갈 수 있는 주소
                        .requestMatchers("/manager/**").hasAnyAuthority("admin", "manager")
                        .requestMatchers("/admin/**").hasAuthority("admin")
                        .anyRequest().permitAll());

        http.formLogin(formLogin ->
                formLogin
                        .disable()); // 폼 태그 로그인 안쓰겠다.

        http.httpBasic(httpBasic ->
                httpBasic
                        .disable()); // 기본적인 HTTP 로그인 안쓰겠다. (ID, PW를 항상 포함하여 요청하는 방식)

        http.apply(new MyCustomDsl());

        return http.build();
    }

    public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
        @Override
        public void configure(HttpSecurity http) throws Exception {

            AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
            http
                    .addFilter(corsConfig.corsFilter())
                    .addFilter(new JwtAuthenticationFilter(authenticationManager));

        }
    }

}
  • addFilterBefore : 시큐리티의 첫 필터는 SecurityContextPersistenceFilter이다. 이 필터 이전에 MyFilter3를 거치도록 한다.
  • formLogin을 꺼놨기에 id, pwd로 로그인을 처리할 필터를 JwtAuthenticationFilter를 구현하여 등록해주어야한다.

이제 addFilter에 등록한 JwtAuthenticationFilter를 보자.

 

 

JwtAuthenticationFilter.java 

package com.example.spotserver.config.jwt;

import com.example.spotserver.config.auth.PrincipalDetails;
import com.example.spotserver.securityStudy.TestUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.BufferedReader;
import java.io.IOException;

// 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter 필터가 있는데
// "/login" 요청해서 유저네임, 패스워드를 post 요청하면 이 필터가 동작함.
// 현재 formLogin을 꺼놨기에 작동 안함.
// 이 필터를 다시 시큐리티 Config에 등록해주면 된다.
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {


    private AuthenticationManager authenticationManager;


    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }


    // "/login" 요청시 로그인 시도를 위해 동작하는 함수
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("로그인 시도중");

        // username, pwd를 받아서 authenticationManager로 로그인 시도를 하면 PrincipalDetailsService가 호출 loadUserByUsername 함수 실
        try {

            ObjectMapper objectMapper = new ObjectMapper();
            TestUser testUser = objectMapper.readValue(request.getInputStream(), TestUser.class);
            System.out.println("testUser = " + testUser);

            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(testUser.getName(), testUser.getPassword());
            //PrincipalDetailsService의 loadUserByUsername()가 실행
            //DB에 있는 name과 pwd가 일치하면 authentication이 리턴됌.
            Authentication authentication =
                    authenticationManager.authenticate(authenticationToken);
            System.out.println("로그인 성공. authentication = " + authentication);
            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
            System.out.println("principalDetails = " + principalDetails.getTestUser());

            // authentication이 세션에 저장됌, 세션 굳이? 권한 관리를 시큐리티가 대신 해주기때문에 편하려고 세션 저장함.
            // JWT 토큰 사용하며 세션을 만들 이유는 없지만 권한처리때문에 session에 넣음
            return authentication;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 3. JWT 토큰을 만들어서 응답해주면 됨.

    }



    // attemptAuthentication 성공시 -> successfulAuth 실행 됌. 여기서 JWT 만들어서 응답하자.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("로그인 성공");
        super.successfulAuthentication(request, response, chain, authResult);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        System.out.println("로그인 실패");
        super.unsuccessfulAuthentication(request, response, failed);
    }
}
  • UsernamePasswordAuthenticationFilter가 "/login"를 처리하는 필터이다.
  • OjectMapper : loginForm을 꺼놨기에 ObjectMapper를 통해 넘어온 JSON을 파싱한다.
  • UsernamePasswordAuthenticationToken : 사용할 아이디와 비밀번호를 넘겨준다.
  • authenticate : 아이디, 비밀번호가 일치한다면 Authentication을 반환한다. 여기에 유저 정보가 담겨져 있다.
  • return Authentication : 세션에 자동으로 담아준다.
  • successfulAuthentication : 로그인 성공시 수행되는 함수이다. 여기서 토큰을 생성하여 응답으로 주자.

 

 

실행 결과

 

 

DB에 admin, zz가 존재하는 상황에 로그인을 시도했다.

 

필터3에 Header의 Authorization : good 인지 검증하는 필터인데 이것을 통과했으며

다음으로 로그인을 시도하여 authentication에 TestUser를 잘 담겨져 있는 모습이다.

Comments