- 지난 포스팅에서 loginForm()메서드를 사용하지 않고 직접 필터를 등록하여 로그인을 시도할 수 있도록 설정하였다. 이번에는 강제 로그인을 진행해보자!
RestApiController 수정
- 로그인 기능을 구현하기 전에 회원가입을 먼저 진행해보자
@PostMapping("join")
public String join(@RequestBody User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setRoles("ROLE_USER");
userRepository.save(user);
return "회원가입완료";
}
JwtApplication 수정
- JwtApplication에 BCryptPasswordEncoder를 빈으로 등록해주자.
@SpringBootApplication
public class JwtApplication {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(JwtApplication.class, args);
}
}
- 프로젝트 실행 후 포스트맨으로 아래 같이 요청을 보내고 '회원가입완료'라는 응답이 오면 회원가입이 정상적으로 이뤄진 것이다.
JwtAuthenticationFilter.java 수정
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : 로그인 시도중");
// 1. username, password를 받아서
try {
BufferedReader br = request.getReader();
String input = null;
while ((input = br.readLine()) != null) {
System.out.println(input);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("============================");
// 2. 정상인지 로그인을 시도해본다. authenticationManager로 로그인을 시도하면
// PrincipalDetailsService의 loadUserByUsername()가 실행됨
// 3. PrincipalDetails를 세션에 담고 (권한 관리를 위해서)
// 4. JWT토큰을 만들어서 응답해주면 됨
return super.attemptAuthentication(request, response);
}
- 이렇게 설정하고 포스트맨으로 '/login'을 시도해보면 회원 정보가 뜨는 것을 확인할 수 있다.
- 그런데 이 방식은 x-www-form방식으로 요청을 하는 것이고, 데이터를 parsing할 때 번거롭다. 요새는 js등을 이용하여 대부분 JSON형식으로 데이터를 주고 받기 때문에 json형식으로 데이터를 보내보자
JwtAuthenticationFilter.java 수정
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : 로그인 시도중");
// 1. username, password를 받아서
// 2. 정상인지 로그인을 시도해본다. authenticationManager로 로그인을 시도하면
// PrincipalDetailsService의 loadUserByUsername()가 실행됨
// 3. PrincipalDetails를 세션에 담고 (권한 관리를 위해서)
// 4. JWT토큰을 만들어서 응답해주면 됨
try {
ObjectMapper om = new ObjectMapper();
User user = om.readValue(request.getInputStream(), User.class);
System.out.println("user = " + user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// PrincipalDetailsService의 loadUserByUsername() 메서드가 실행된 후
// 정상처리 되면 authentication이 리턴 됨
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
// Authentication 객체가 session영역에 저장됨
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
// 아래 출력문이 제대로 찍혔다는 것은 로그인이 되었다는 뜻.
System.out.println("로그인 완료 됨 = " + principalDetails.getUser().getUsername());
// authentication 객체가 session영역에 저장을 해야하고 그 방법이 authencitaion을 리턴해주면 된다.
// 리턴해주는 이유는 권한 관리를 security가 대신 해주기 때문에 편하려고 하는 것이고
// 이게 아니라면 jwt를 사용하면서 세션을 생성할 이유가 없다.
return authentication;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
- json 데이터와 user 엔티티가 매핑되어 데이터가 파싱된 것을 확인할 수 있다.
- 이제 jwt를 생성해보자!!
JwtAuthenticationFilter.java에 메서드 추가
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication이 실행됨 => 인증이 완료됨");
super.successfulAuthentication(request, response, chain, authResult);
}
- attemptAuthentication실행 후 인증이 정상적으로 되었다면 successfulAuthentication가 실행된다.
여기서 JWT를 생성하여 request한 사용자에게 토큰을 반환해주면 된다.
- 일단 정상 동작하는지 확인해보자
- 인증이 정상적으로 처리되었을 때 successfulAuthentication()메서드가 정상적으로 수행되는 것을 확인할 수 있다
- 인증이 정상적으로 처리되지 않았을 경우, 자격 증명에 실패했다는 로그를 확인할 수 있다!!!
JwtAuthenticationFilter.java 수정
- successfulAuthentication에 JWT토큰 생성 관련 코드를 추가해주자
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication이 실행됨 => 인증이 완료됨");
// 추가
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
// RSA방식은 아니구 Hash암호방식
String jwtToken = JWT.create()
.withSubject("pem토큰")
.withExpiresAt(new Date(System.currentTimeMillis() + (60000 * 10)))
.withClaim("id", principalDetails.getUser().getId())
.withClaim("username", principalDetails.getUser().getUsername())
.sign(Algorithm.HMAC256("pem"));
response.addHeader("Authorization", "Bearer "+jwtToken);
}
- 여기서 인자값으로 받은 authResult는 JwtAuthenticationFilter에서 생성된 authentication이다.
- 이제 다시 포스트맨으로 요청을 보내보자
- header에 Authorization의 value로 토큰이 들어가있다.
- 원래 username과 password가 일치하면 http body에는 공백이 떠야하고, password가 틀렸다면 401이 떠야한다. 그런데 나는 자꾸 404가 뜨는데 헤더에 토큰은 또 받아진다ㅜㅜ
- 이런 경우 SecurityConfig를 수정해주자
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 아래 부분 한 줄 주석 처리
// http.addFilterBefore(new MyFilter3(), SecurityContextPersistenceFilter.class);
return http
- 이것으로 jwt생성까지 완료하였다. 이제 사용자가 jwt를 가지고 요청을 할 때 이 토큰이 유효한지를 판단하는 필터를 만들어야 한다.
세션 방식 vs jwt방식
- 세션 방식
- 로그인 시도 후 성공하면 서버쪽에서 세션을 생성하고 서버에 저장을 한 후, 세션 id를 쿠키에 담아 클라이언트에게 함께 보낸다
- 이후 클라이언트가 다시 요청을 보내면 서버는 쿠키값의 세션 id가 유효한지 서버의 세션 id와 비교하고, 인증이 필요한 페이지로 접근하게 하면 된다.
- JWT방식
- 로그인 시도 후 성공하면 서버쪽에서 jwt를 생성하고, jwt를 클라이언트에게 보낸다
- 이후 클라이언트가 다시 요청을 할 때 jwt를 함께 보내고, 서버는 jwt가 유효한지 필터를 이용해 판단을 한다.