[JWT] jwt 로그인 및 토큰 생성

devkwon·2024년 1월 19일

로그인

스프링 시큐리티에는 UsernamePasswordAuthenticationFilter라는 필터가 존재한다. 이는 기본적으로 /login에 username,password를 전송하면 동작을 한다. 허나 formLogin을 disable하면 작동하지 않는다.

이를 작동하게 하려면 다시 securityconfig에 등록을 해줘야한다.

@Configuration
@EnableWebSecurity 
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    
    //AuthenticationManager 생성
        AuthenticationManagerBuilder sharedObject = http.getSharedObject(AuthenticationManagerBuilder.class);

        sharedObject.userDetailsService(userDetailsService);
        AuthenticationManager authenticationManager = sharedObject.build();

        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션을 stateless로 관리
                .httpBasic(AbstractHttpConfigurer::disable) // 기본적인 로그인 기능 사용 x
                .formLogin(AbstractHttpConfigurer::disable)
                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {
                    authorizeRequests.requestMatchers("/api/v1/user/**").hasAnyRole("ADMIN", "MANAGER","USER");

                    authorizeRequests.requestMatchers("/api/v1/manager/**")
                            .hasAnyRole("ADMIN", "MANAGER");

                    authorizeRequests.requestMatchers("/api/v1/admin/**")
                            .hasRole("ADMIN");

                    authorizeRequests.anyRequest().permitAll();
                })
                .cors((cors)->{
                    CorsConfiguration config = new CorsConfiguration();
                    config.setAllowedOrigins(List.of("*"));
                    config.setAllowedHeaders(List.of("*"));
                    config.setAllowedMethods(List.of("*"));
                    config.setAllowCredentials(true);
                    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                    source.registerCorsConfiguration("/api/**",config);
                    cors.configurationSource(source);
                })

                .addFilterBefore(new MYFilter3(), SecurityContextHolderFilter.class)
                .authenticationManager(authenticationManager)
                .addFilter(new JwtAuthenticationFilter(authenticationManager)); //AuthenticationFilter 등록

        return http.getOrBuild();
    }
}

UsernamePasswordAuthenticationFilter에는 attemptAuthentication라는 메소드가 존재하는데, 로그인을 시도할 때 실행된다.

이제 UsernamePasswordAuthenticationFilter 필터를 보자

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    // /login 요청을 하면 로그인 시도를 위해서 실행되는 함수
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("attempt login");

        // 1. username, password 받기
        try {
            ObjectMapper om = new ObjectMapper();
            User user = om.readValue(request.getInputStream(),User.class);
            System.out.println(user);

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
            System.out.println(authenticationToken);
            // PrincipalDetailsService의 loadUserByUsername()이 실행됨.
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            // authentication이 들어왔다는건 DB에 username과 password가 일치하는 것을 찾았다는 것.

            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
            System.out.println(principalDetails.getUser().getUsername());
            // 이제 Authentication 객체가 세션영역에 저장됨. 즉, 로그인이 되었다는 뜻.
            return authentication; // 저장을 위해 리턴

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 2. 정상인지 로그인 시도
        // authenticationManager로 로그인 시도를 하면 PrincipalDetailsService가 호출됨.
        // loadUserByUsername()이 호출됨.


        // 3. PrincipalDetails를 세션에 담기
        // 담지 않으면 권한관리가 되지 않음.


        // 4. JWT 토큰을 만들어서 응답.

    }
  1. json 형식으로 된 username, password를 받아오면 ObjectMapper를 통해 User 클래스로 파싱을 한다.
  2. UsernamePasswordAuthenticationToken에 username과 password를 넣어서 DB에 해당하는 유저가 있는지 확인한다.
  3. 만약에 존재한다면 AuthenticationToken이 생성될 것이고 이를 기반으로 authenticationManager가 authentication을 만들어서 리턴할 것이다.

이렇게 로그인이 되면 successfulAuthentication()가 호출이 된다.

토큰 생성

이제 successfulAuthentication()을 활용해서 로그인 후 사용자에게 줄 토큰을 만들어보자.

@Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("successfulAuthentication :  인증 완료!" );

        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        //Hash암호 방식
        String jwtToken = JWT.create()
                .withSubject("token")
                .withExpiresAt(new Date(System.currentTimeMillis()+(60000*10))) // 토큰의 만료시간
                .withClaim("id",principalDetails.getUser().getId())
                .withClaim("username",principalDetails.getUser().getUsername())
                .sign(Algorithm.HMAC512("secret"));

        response.addHeader("Authorization","Bearer "+jwtToken);
    }

이제 로그인을 하면

header에 성공적으로 Authorization에 JWT값이 들어갔다.

0개의 댓글