🔥스파르타 TIL (Spring)

Spring Security + JWT 로그인 구현하기

승승장규 2025. 2. 25. 15:48
 

Spring Security 란?

스프링 기반의 애플리케이션의 보안을 담당하는 프레임워크이다.필터 기반으로 동작하기 때문에 스프링 MVC와 분리되어 관리 및 동작한다. Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고

seungg8361.tistory.com

Controller에서 @PostMapping("/login")을 처리하지 않고 filter에서 로그인을 처리해 보자.

 

먼저 Spring Security 관련 작업을 진행해 보자

// Spring Security에서 사용자의 상세 정보를 담는 클래스 -> 실제 인증된 사용자의 정보를 담고있다.
public class UserDetailsImpl implements UserDetails {

    private final User user;
    
    // UserDetails 인터페이스의 메서드
    @Override
    public String getPassword() {return user.getPassword();}
    @Override
    public String getUsername() {return user.getUsername();}
    
    // 사용자의 권한 정보를 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {return true;}
    @Override
    public boolean isAccountNonLocked() {return true;}
    @Override
    public boolean isCredentialsNonExpired() {return true;}
    @Override
    public boolean isEnabled() {return true;}
}

 

GrantedAuthority : 사용자의 권한을 나타내는 인터페이스로 SimpleGrantedAuthority는 GrantedAuthority의 기본 구현체다. 우선 SimpleGrantedAuthority로 권한을 생성하고, Collection으로 여러 권한을 관리할 수 있다.

 

// Spring Security에서 사용자 정보를 가져오는 서비스 클래스
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

	// UserDetailsService의 loadUserByUsername 메서드를 활용해서 User를 찾음
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user);
    }
}

 

인증 과정을 간단하게 보자면

1. 사용자가 로그인을 시도한다.

POST /login
username : user
password : 1234

   

2. UsernamePasswordAuthenticationFilter 동작

// Spring Security의 기본 인증 필터가 요청을 가로채서 token 생성
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);

 

3. AuthenticationManager 호출

// AuthenticationManager가 인증 처리
public Authentication authenticate(Authentication authentication){
}

 

4. UserDetailsService 호출해서 UserDetails 구현체 반환

 

5. 비밀번호 검증이 끝나고 인증이 완료되면 SecurityContext 설정

// 인증이 성공하면 Authentication 객체가 SecurityContext에 저장됨
SecurityContextHolder.getContext().setAuthentication(authentication);

 

이제 filter에서 로그인을 처리해 보자

 

JWT 활용하기 (2) - 회원 가입, 로그인 기능 만들기

회원 가입 기능을 구현하기에 앞서 JWT 관련해서 간단한 설정들을 알아보자  JWT 활용하기 (1)인증(Authentication)과 인가(Authorization) 란? 인증(Authentication)과 인가(Authorization) 란?인증 ▼해당 유저가

seungg8361.tistory.com

 

filter에서 로그인을 처리할 클래스

// UsernamePasswordAuthenticationFilter의 기능들을 사용하기 위해 상속받은 filter 클래스
// 위에 1~3 번까지의 인증과정을 수행하는 filter로 jwt방식을 사용하기 때문에 직접 커스텀해서 사용
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/user/login"); // 생성자 호출 시 로그인 엔드포인트 작성
    }
    // 로그인을 시도하는 메서드
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
        // JSON 형태의 String 데이터를 Object로 바꿈. POST, PUT 등의 요청에서 전송된 데이터를 읽기 위해
        // getInputStream()을 사용해서 바이트 스트림 형태로 데이터 반환
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
// UsernamePasswordAuthenticationFilter의 getAuthenticationManager를 호출해서 token을 만들어서 반환
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }
    // 로그인이 성공했을 때 수행되는 메서드, Authentication인증 객체를 가져온다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
        // Authentication에는 UserDetailsImpl이 담겨있다. getPrincipal()을 캐스팅
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
    }
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(401);
    }
}

 

인가를 진행할 클래스

// 동일한 요청에 대해 필터가 여러번 실행되는 것을 방지하기 위해 OncePerRequestFilter사용
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
        // 순수한 토큰을 바로 뽑아냄.
        String tokenValue = jwtUtil.getJwtFromHeader(req);

        if (StringUtils.hasText(tokenValue)) {
            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }
        filterChain.doFilter(req, res);
    }
    // 인증 처리
    public void setAuthentication(String username) {
    // 보안 주체의 세부 정보를 포함
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }
    // 인증 객체 생성
    private Authentication createAuthentication(String username) {
    // 저장되어 있는 인증 객체 가져오기
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

직접 filter를 구현해서 인증, 인가 처리를 진행하였으면 Spring Security Config를 수정해 보자

public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    // AuthenticationManager를 세팅하기 위해 주입 받아옴.
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

// AuthenticationManager 생성하고 등록하는 메서드
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
    // JwtAuthenticationFilter 생성자를 활용하기 위해
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        // getAuthenticationManager() 으로 가져왔으므로 setAuthenticationManager()로 만들어줌
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/").permitAll() // 메인 페이지 요청 허가
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );
        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );
        // JwtAuthenticationFilter가 수행되기 전에 jwtAuthorizationFilter를 수행해서 먼저 인가 단계를 거침
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        // UsernamePasswordAuthenticationFilter가 수행되기 전에 jwtAuthenticationFilter를 먼저 실행
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

이제 Controller에서 인증된 유저의 정보를 가져오는 방법을 보자

@GetMapping("/")
// @AuthenticationPrincipal -> Spring Security에서 현재 인증된 사용자의 정보를 받아올 때 사용
public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails){
    model.addAttribute("username", userDetails.getUsername());
    return "home";
}

 

 

이상으로 Spring Security와 JWT를 활용해서 로그인 기능을 구현해 보았습니다.🖐️🖐️

'🔥스파르타 TIL (Spring)' 카테고리의 다른 글

Pageable을 알아보자  (0) 2025.02.25
QueryDSL 이란?  (0) 2025.02.25
RestTemplate란?  (0) 2025.02.24
JWT 활용하기 (2) - 회원 가입, 로그인 기능 만들기  (0) 2025.02.13
Spring Security 란?  (0) 2025.02.10