Spring Security JWT 로그인 및 토큰 반환

바그다드·2023년 4월 19일
0

Spring Security

목록 보기
16/17
  • 지난 포스팅에서 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한 사용자에게 토큰을 반환해주면 된다.
  • 일단 정상 동작하는지 확인해보자
  1. 인증이 정상적으로 처리되었을 때 successfulAuthentication()메서드가 정상적으로 수행되는 것을 확인할 수 있다

  2. 인증이 정상적으로 처리되지 않았을 경우, 자격 증명에 실패했다는 로그를 확인할 수 있다!!!


  • 이제 진짜 jwt를 생성해보자!!

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방식

  1. 세션 방식
  • 로그인 시도 후 성공하면 서버쪽에서 세션을 생성하고 서버에 저장을 한 후, 세션 id를 쿠키에 담아 클라이언트에게 함께 보낸다
  • 이후 클라이언트가 다시 요청을 보내면 서버는 쿠키값의 세션 id가 유효한지 서버의 세션 id와 비교하고, 인증이 필요한 페이지로 접근하게 하면 된다.
  1. JWT방식
  • 로그인 시도 후 성공하면 서버쪽에서 jwt를 생성하고, jwt를 클라이언트에게 보낸다
  • 이후 클라이언트가 다시 요청을 할 때 jwt를 함께 보내고, 서버는 jwt가 유효한지 필터를 이용해 판단을 한다.
profile
꾸준히 하자!

0개의 댓글