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를 위한 필터를 탔기 때문에 그 후로 토큰을 발급받고 토큰을 검증하는 것을 구현하지 못하였기 때문에 컨트롤러에 도달하지 못함이 아닌가 싶다
- 일단 나머지 것을 구현해보고 다시 판단해봐야 할 듯 하다