포스트

240227 기록, jwt적용

JWT 적용하기

Security Config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import com.example.userservice.service.MyUserDetailsService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {

  private final MyUserDetailsService myUserDetailsService;

  // 스프링 시큐리티에서 제공하는 인증, 인가를 위한 필터 모음
  // Application Context 초기화가 이루어 지면서 HttpSecurity 객체가 설정한 filterChain 형성

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
              .authorizeHttpRequests(
                      (authorizeRequest) -> authorizeRequest
                              .requestMatchers("/", "/api/users/login", "/api/users/loginProc", "/api/users/join" , "/api/users/joinProc").permitAll()
                              .requestMatchers("/api/users/admin").hasRole("ADMIN")
                              .anyRequest().authenticated()
              );

      http
              .csrf(AbstractHttpConfigurer::disable);
              // == .csrf((auth) -> auth.disable());
              // CSRF(Cross-Site-Request Forgery 보호를 비활성화하는 메서드 호출
              // AbstractHttpConfigurer::disable -> AbstractHttpConfigurer에 정의된 disable 메소드에 대한 참조

      /*
      JWT가 없는 상태에서 시큐리티만 적용 , form 로그인을 통한 로그인
      JWT를 사용하기 위해 주석 처리
      http
              .formLogin((auth) -> auth.loginPage("/api/users/login")
                      .loginProcessingUrl("/loginProc")
                      .permitAll());
      */

      // JWT를 사용하기 위해 로그인 방식 두가지 disable
      http
              .formLogin(AbstractHttpConfigurer::disable);
      // == formLogin((auth) -> auth.disable());

      http
              .httpBasic(AbstractHttpConfigurer::disable);

      // 세션 설정. STATELESS 상태로 변경
      http
              .sessionManagement((session) -> session
                      .sessionCreationPolicy(SessionCreationPolicy.STATELESS));


      return http.build();
  }

  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
  }
}
  • jwt 방식의 로그인을 하는데 formLogin과 httpBasic을 왜 disable해야하는지 의문이 들었다.
  • 왜냐하면 어차피 jwt를 해도 form 로그인을 진행하는데라는 생각이 있었기 때문이다
  • 찾아본 이유는
  • formLogin은 세션 로그인 방식에서 로그인을 자동처리 해준다는 장점이 있지만,
  • jwt에서는 로그인 방식 내에 jwt 토큰을 생성하는 로직이 필요하다
  • 따라서 로그인 과정을 수동으로 클래스를 만들어줘야 하기 때문에 formLogin 기능을 disable 시키는 것이다
LoginFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
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.IOException;

@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
// UsernamePasswordAuthenticationFilter는 원래 HttpSecurity#formLogin에서 진행을 했는데
// jwt를 위해 formLogin을 disable 시켰기 때문에 개발자가 구현해줘야 한다

  private final AuthenticationManager authenticationManager;

  @Override
  // 미승인 Authentication 생성하는 메소드
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{

    // 요청을 가로채서 username과 password를 추출
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    // 필터를 타는지 확인    
    System.out.println("username : " + username);

    // 스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 한다
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
                                                                                        // username, password, role
    // token에 담은 검증을 AuthenticationManager로 전달
    return authenticationManager.authenticate(authToken);
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    System.out.println("로그인 성공");
  }

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    System.out.println("로그인 실패");
  }
}
UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import com.example.userservice.dto.JoinDto;
import com.example.userservice.dto.LoginDto;
import com.example.userservice.dto.ResponseUserDto;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;


@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {

  private final UserService userService;
  private ModelAndView mav;

  // 로그인 페이지
  @GetMapping("/login")
  public ModelAndView loginP(){
    mav = new ModelAndView("login");
    return mav;
  }

  @PostMapping("/loginProc")
  public ModelAndView loginProccess(LoginDto loginDto){
    ResponseUserDto responseUserDto =  userService.loginProccess(loginDto);

    String role = responseUserDto.getRole();

    mav = new ModelAndView();

    if(role.equals("ROLE_USER")) mav.setViewName("userMain");
    else if(role.equals("ROLE_ADMIN")) mav.setViewName("adminMain");

    return mav;
  }

  // 회원가입 페이지
  @GetMapping("/join")
  public ModelAndView joinP(){
    mav = new ModelAndView("join");
    return mav;
  }

  // 회원가입
  @PostMapping("/joinProc")
  public ModelAndView joinProccess(JoinDto joinDto){

    userService.joinProccess(joinDto);

    mav = new ModelAndView("login");
    return mav;
  }
}
트러블슈팅
  • 포스트맨을 사용하여 /api/users/loginProc를 호출하면서 body에 데이터를 넣었다
  • 포스트맨의 응답영역에 로그인은 이뤄져서 userMain 또는 adminMain으로 이동했다는 응답은 받았는데
  • 인텔리제이의 콘솔창에 로그인 필터를 타는지 확인하기 위한 username을 println 한 것이 뜨지 않았다.
  • 필터를 타지 않는다는 것이었다.
  • 확인을 해보니 내가 참고하였던 강의에서는 login url을 따로 구현하지 않고, 기본 시큐리티에서 처리되는 /login url을 사용하였고
  • 포스트맨으로 id와 pw를 post로 보내어 필터를 타는 것이었다.
  • 나는 이미 로그인 로직과 컨트롤러에서 맵핑을 해놨기 때문에 시큐리티를 타지 않고 내가 정한 url로 맵핑이 이뤄지면서 로그인 필터가 작용이 되도록 세팅을 해야했다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import com.example.userservice.jwt.LoginFilter;
import com.example.userservice.service.MyUserDetailsService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {

  private final MyUserDetailsService myUserDetailsService;

  private final AuthenticationConfiguration authenticationConfiguration;

  // 로그인필터의 생성자를 위해
  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
      return configuration.getAuthenticationManager();
  }


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

  // 스프링 시큐리티에서 제공하는 인증, 인가를 위한 필터 모음
  // Application Context 초기화가 이루어 지면서 HttpSecurity 객체가 설정한 filterChain 형성

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    // JWT를 사용하기 위해 로그인 방식 두가지 disable
    http
            .formLogin(AbstractHttpConfigurer::disable);
    // == formLogin((auth) -> auth.disable());

    http
            .httpBasic(AbstractHttpConfigurer::disable);

    http
            .csrf(AbstractHttpConfigurer::disable);
    // == .csrf((auth) -> auth.disable());
    // CSRF(Cross-Site-Request Forgery 보호를 비활성화하는 메서드 호출
    // AbstractHttpConfigurer::disable -> AbstractHttpConfigurer에 정의된 disable 메소드에 대한 참조

    http
            .authorizeHttpRequests(
                    (authorizeRequest) -> authorizeRequest
                            .requestMatchers("/", "/api/users/login", "/api/users/loginProc", "/api/users/join" , "/api/users/joinProc").permitAll()
                            .requestMatchers("/api/users/admin").hasRole("ADMIN")
                            .anyRequest().authenticated()
            );

    // jwt 구현으로 인해 수동적으로 loginFilter를 만들었기 때문에 시큐리티에서 filter를 인식하도록 해야함
    // 필터등록은 addFilter가 있는데
    // addFilterAt() 은 그 자리에 등록 , addFilterAfter()는 특정 필터 뒤에 등록 , addFilterBefore() 특정 필터 전에 등록
    // 첫번째 인자는 무슨 필터를, 두번째 인자는 위치
    // UsernamePasswordAuthenticationFilter의 역할을 수동적으로 만들어줬기 때문에 위치가 여기가 됨

    LoginFilter loginFilter = new LoginFilter(authenticationManager(authenticationConfiguration));
    loginFilter.setFilterProcessesUrl("/api/users/loginProc");
    http
            .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);


    /*
    JWT가 없는 상태에서 시큐리티만 적용 , form 로그인을 통한 로그인
    JWT를 사용하기 위해 주석 처리
    http
            .formLogin((auth) -> auth.loginPage("/api/users/login")
                    .loginProcessingUrl("/loginProc")
                    .permitAll());
    */

    // 세션 설정. STATELESS 상태로 변경
    http
            .sessionManagement((session) -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    return http.build();
  }

}
  • 로그인 필터를 세팅하는 곳에 로그인필터 객체를 미리 만들고 필터 접근 url을 셋팅한 후
  • 필터를 등록하였다.
  • 이제는 필터를 탔기 때문에 인텔리제이 콘솔창에 username과 로그인 성공이라는 문구가 출력되었다.
  • 하지만 필터를 타기 전에는 즉, 그냥 컨트롤러를 통해 DB에서 조회하여, 역할의 값을 넣었고 역할에 따라 user , admin 페이지로 이동되는 결과가 나오지 않았다.
  • 이것은 jwt를 위한 필터를 탔기 때문에 그 후로 토큰을 발급받고 토큰을 검증하는 것을 구현하지 못하였기 때문에 컨트롤러에 도달하지 못함이 아닌가 싶다
  • 일단 나머지 것을 구현해보고 다시 판단해봐야 할 듯 하다
깃 주소

GitHub