본문 바로가기
Spring/Security

[JWT] JWT 인증, 인가 필터 생성 / 회원 정보 가져오기 / 인증 객체로 로그인 처리 (2)

by 행운의나무 2022. 10. 28.
728x90
반응형

목표

  • 회원가입 기능
  • 로그인 기능
  • 로그인 시 JWT 토큰 발급
  • JWT 인증, 인가 필터 생성
  • JWT의 회원 정보 가져오기
  • 인증 객체로 로그인 처리

이전 글 - 회원가입 / 로그인 / JWT 토큰 발급

2022.10.28 - [Spring/Security] - [JWT] 회원가입 / 로그인 / 토큰 발급 (1)

 

[JWT] 회원가입 / 로그인 / 토큰 발급 (1)

목표 회원가입 기능 로그인 기능 로그인 시 JWT 토큰 발급 JWT 인증, 인가 필터 생성 JWT의 회원 정보 가져오기 또 다른 로그인 기능 환경 java 17 spring boot 2.7 gradle 7.5 환경 설정 1. build.gradle - Web,..

twer.tistory.com

JWT 인증, 인가 필터 생성

인증을 위한 객체, 서비스 생성

PrincipalDetails

- UserDetails를 구현하는 객체
- Entity 클래스인 User에 직접적으로 UserDetails를 구현하는 것은 좋은 방식이 아니다.
- User 객체를 받아 UserDetailsService로 넘겨 주는 역할을한다.

/**
 * UserDetails를 이용해서 User객체에 대한 정보를 검색한다.
 * UserDetailsService는 인터페이스이므로, 우리가 인증하고자하는 비즈니스로직을 정의한 serivce레이어에서 구현을 실행하는 방식을 이용한다.
 */
@Data
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final User user;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        user.getRolesList().forEach(r -> {
            authorities.add(() -> r);
        });

        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

PrincipalDetailsService

- UserDetailsService를 구현한다.
- username 기반의 userDetails를 검색한다.

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User userEntity = userRepository.findByEmail(email);

        return new PrincipalDetails(userEntity);
    }
}

필터 생성

JwtAuthenticationFilter

- 인증(로그인)을 위한 필터
- Spring Security에서 /login 함수 호출 시 이용된다. -> Authentication을 반환한다.

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {


    private final AuthenticationManager authenticationManager;

    private final JwtProvider jwtProvider;

    // 인증 객체(Authentication)을 만들기 시도
    // attemptAuthentication 추상메소드의 구현은 상속한 UsernamePasswordAuthenticationFilter에 구현 되어 있습니다.
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {

            ObjectMapper om = new ObjectMapper();
            User user = om.readValue(request.getInputStream(), User.class);

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword()
            );

            /*
            - Filter를 통해 AuthenticationToken을 AuthenticationManager에 위임한다.
               UsernamePasswordAuthenticationToken 오브젝트가 생성된 후, AuthenticationManager의 인증 메소드를 호출한다.
            - PrincipalDetailsService의 loadUserByUsername() 함수가 실행된다.
            => 정상이면 authentication이 반환된다.
            * */
            Authentication authentication = authenticationManager.authenticate(authenticationToken);

            /*
            - authentication 객체가 session 영역에 정보를 저장한다. -> 로그인 처리
            - authenticatino 객체가 세션에 저장한 '방식'을 반환한다.
            - 참고 : security가 권한을 관리해주기 때문에 굳이 JWT에서는 세션을 만들필요는 없지만, 권한 처리를 위해 session을 사용한다.
             */

            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
            System.out.println("Authentication 확인: "+principalDetails.getUser().getUsername());

            return  authentication;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    // attemptAuthentication 메소드가 호출 된 후 동작
    // response에 JWT 토큰을 담아서 보내준다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

       PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        Long id = principalDetails.getUser().getId();
        String email = principalDetails.getUser().getEmail();
        String userName = principalDetails.getUser().getUsername();

        String jwtToken = jwtProvider.generateJwtToken(id, email, userName);

        response.addHeader("Authorization", "Bearer " + jwtToken);

    }
}

 

JwtAuthorizationFilter

- 인증이나 권한이 필요한 주소 요청이 있을때 해당 필터를 통과한다.
- Jwt를 검증하고 SpringSecurityContext에 세션을 저장한다.

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private JwtProvider jwtProvider;

    private PrincipalDetailsService principalDetailsService;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider, PrincipalDetailsService principalDetailsService ) {
        super(authenticationManager);
        this.jwtProvider = jwtProvider;
        this.principalDetailsService = principalDetailsService;
    }

    // 인증이나 권한이 필요한 주소 요청이 있을 때 해당 필터를 통과한다.
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String jwtHeader = request.getHeader("Authorization");

        // header가 있는지 확인
        if (jwtHeader == null || !jwtHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);

            return;
        }

        // JWT 토큰을 검증하여 정상적인 사용자인지 확인
        String jwtToken = request.getHeader("Authorization").replace("Bearer ","");
        User tokenUser = jwtProvider.validToken(jwtToken);
        if( tokenUser != null){
            PrincipalDetails principalDetails = new PrincipalDetails(tokenUser);

            // JWT 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
            Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());


            // 시큐리티 세션에 Authentcation 을 저장한다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }


        chain.doFilter(request, response);
    }
}

 

SecurityConfig

- 만들어진 필터들을 등록한다.

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final PrincipalDetailsService principalDetailsService;

    private final UserRepository userRepository;

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

 // ...
    // 시큐리티 필터 설정
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .cors().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable() // http의 기본 인증. ID, PW 인증방식
                .addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtTokenProvider()))  // AuthenticationManager
                .addFilter(new JwtAuthorizationFilter(authenticationManager(),  jwtTokenProvider(), principalDetailsService))  // AuthenticationManager
                .authorizeHttpRequests()
                .anyRequest().permitAll();


        return http.build();

    }
}

JWT의 회원 정보 가져오기

controller

- JWT의 정보를 가져올 수 있는 방법은 두 가지이다.
- @AuthenticationPrincipal을 이용해 SecurityContext에 있는 세션을 가져오는 방법
- Authentcation 객체를 이용해 AuthenticatioManager, UserDetailsService을 통해 가져오는 방법

    @GetMapping("/api/info")
    public String info(@AuthenticationPrincipal PrincipalDetails principalDetails, Authentication authentication) {
        System.out.println("PrincipalDetails " + principalDetails);
        System.out.println("authentication " + authentication);

        StringBuilder sb = new StringBuilder();
        sb.append("PrincipalDetails ");
        sb.append(principalDetails);
        sb.append("\n\n");
        sb.append("authentication ");
        sb.append(authentication);

        return sb.toString();

    }

- JWT 토큰을 Header 값에 "Authorization"을 key값으로 넣고, value값에 "Bearer " (한 칸 띄어야함. 주의)을 붙여 로그인한 토큰을 붙인다.

로그인 api 호출로 토큰 값을 가져와 복사한다.
토큰 값을 넣어 회원 정보를 볼 수 있다.

PrincipalDetails PrincipalDetails(user=User(id=12, username=username1, email=test1@test.com, password=$2ayi, roles=ROLE_USER, createdDate=2022-10-28T13:02:30.878808))

authentication UsernamePasswordAuthenticationToken [Principal=PrincipalDetails(user=User(id=12, username=username1, email=test1@test.com, password=$2ayi, roles=ROLE_USER, createdDate=2022-10-28T13:02:30.878808)), Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[unit.water.jwt_exam.auth.PrincipalDetails$$Lambda$1201/0x000000080147d278@2d9e1306]]

다음과 같이 @AuthenticationPrincipal 어노테이션을 이용하여 PrincipalDetails 객체를  가져온 값은 PrincipalDetails로 반환해주고, Authentication객체로 가져온 값은 UsernamePasswordAuthenticationToken형태의 인증 객체로 반환해줍니다.

참고 : 만약 Spring Security를 사용하지 않는다면, @AuthenticationPrincipal를 사용할 수 없다. -> ArgurmentResolver나 Interceptor등의 추가 개발을 통해 정보를 가져와야 합니다.


인증 객체로 로그인 처리

service

- PrincipalDetails 객체와 PrincipalDetailsServie 서비스가 있으면 로그인 처리를 다르게 할 수 있습니다.
- 패스워드를 인코딩하여 매칭 시키는 작업을 피할 수 있습니다.

@Service
@RequiredArgsConstructor
public class UserService {

	// ...
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    
    // ...
    public String login(LoginRequestDto loginRequestDto) {
//        String email = loginRequestDto.getEmail();
//        String rawPassword = loginRequestDto.getPassword();
//
//        User byEmail = userRepository.findByEmail(email);
//
//        // 비밀번호 일치 여부 확인
//        if(passwordEncoder.matches(rawPassword, byEmail.getPassword())){
//
//            // JWT 토큰 반환
//            String jwtToken = jwtProvider.generateJwtToken(byEmail.getId(), byEmail.getEmail(), byEmail.getUsername());
//
//            return "로그인 성공 " + jwtToken;
//        }

        // AuthenticationManager에서 처리하는 방법
        // AuthentcationManager는 UserDetails로 User 테이블에 접근하여 회원 인증이 가능하다.

        String email = loginRequestDto.getEmail();
        String password = loginRequestDto.getPassword();

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 인증이 완료된 객체이면,
        if(authentication.isAuthenticated()) {
            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();

            Long authenticatedId  = principalDetails.getUser().getId();
            String authenticatedEmail = principalDetails.getUser().getEmail();
            String authenticatedUsername = principalDetails.getUser().getUsername();

            return "로그인 성공 " + jwtProvider.generateJwtToken(authenticatedId, authenticatedEmail, authenticatedUsername);
        }

        return "로그인 실패";
    }
}

인증 객체로 로그인 처리

함께보면 도움되는 글

2022.10.21 - [Spring/Security] - [Security] 스프링 시큐리티의 아키텍처(구조) 및 흐름

 

[Security] 스프링 시큐리티의 아키텍처(구조) 및 흐름

Spring Security 스프링 시큐리티리란? 어플리케이션의 보안(인증 및 권한)을 담당하는 프레임워크 Spring Security를 사용하지 않으면 자체적으로 세션을 체크해야 한다. redirect를 일일이 설정해주어야

twer.tistory.com

2022.09.05 - [Spring/Security] - [Security] WebSecurityConfigurerAdapter Deprecated

 

[Security] WebSecurityConfigurerAdapter Deprecated

버전 Java 11 Spring boot 2.7.3 Deprecated Spring Security 5.7.0-M2부터 Deprcated 되었다. (SpringBoot 기준 2.7 이후) WebSecurityConfigurerAdapter : WebSecurityConfigurer의 인스턴스를 생성하여 Abstrac..

twer.tistory.com

쿠팡으로 연결 클릭

 

제주삼다수 그린

COUPANG

www.coupang.com

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음

반응형