로그인한 상태에서 토큰을 이용하여 member 정보 불러와서 이용하기(2)

이정연·2023년 6월 7일
0

project034

목록 보기
9/10

문제인식

  • 로그인 후 마이페이지에서 내가 쓴 글들을 조회하기위해서는 해당 글들을 누가 썼는지에 대한 사용자의 id가 부여되어있을것이다.

  • 그 id를 직접 API에 노출시켜서 조회 메서드를 만들었었는데, 이번에는 위험을 줄이고자 me로 통일하여 관리하기로 했다.

  • 프로젝트를 진행하며 토큰을 이용하여 member-id를 불러오기위한 방법을 소개하고자 한다.

  • 우리 프로젝트의 마이페이지에는 자신이 쓴글의 Title만 조회하면되기때문에 URI path는 "/me/questionsTitle"로 설정했다.

방법1

  • 처음 Controller 에서 시작하여 토큰에 저장되어있는 principal에서 username을 가져오려고 계획했다.
  • SpringSecurity에서 코드를 보면 SpringSecurity 에서 인식할 수 있는 username을 Member클래스의 email 주소로 채우고있다.
  • 보통 토큰의 경우 email 정보를 username으로 저장하기때문에 토큰을 이용하여 처음 가져오는 값은 usermame인 eamil 정보가된다. 그래서 username을 이용하여 id를 가져오는 과정이 필요했다.
private final class MemberDetails extends Member implements UserDetails{
        MemberDetails(Member member){
            setId(member.getId());
            setNickname(member.getNickname());
            setEmail(member.getEmail());
            setPassword(member.getPassword());
            setRoles(member.getRoles());
        }
        @Override
        //유저의 권한 정보 생성
        public Collection<? extends GrantedAuthority> getAuthorities(){
            //DB에 저장된 Role 정보로  User 권한 목록 생성
            return authorityUtils.createAuthorities(this.getRoles());
        }
        @Override
        public String getUsername(){
            return getEmail(); <-------------------------------------
        }
}
  • login 할때, token 발행(생성)에 추가되는 User 정보는 아래와 같다.
private String delegateAccessToken(Member member){
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", member.getEmail());
        claims.put("roles", member.getRoles());
        .
        .
        .
}

간단하게 옮겼지만, 위의 코드를 보면 username에 Email이 들어가고있다.

구조

  • 전역에 선언된 SecurityContextHolder을 이용하여 username을 가져오는 방법을 택했다.

Controller

@GetMapping("/me/questionsTitle")
public ResponseEntity<List<QuestionResponseDto>> getMyQuestionsTitles(){
	Long id = Long.parseLong((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();<----------
    return ResponseEntity.ok(memberService.getMyQuestionsTitle(id));

Service

public List<QuestionResponseDto> getMyQuestionsTitles(Long id){
	list<QuestionRespinseDto> QuestionDtoList = new ArrayList<>();
    List<Question> questionList = questionRepository.findAllByMemberId(id);
    
    for(Question question : questionList){
    	QuestionDtoList.add(
        	QuestionResponseDto.builder()
            	.title(question.getTitle())
                .build());
    }
    return QuestionDtoList;
}

Repository

@Repository
public interface QuestionRepository entends JpaRepository<Question, Long> {
	Page<Question> findByMemberId(Long id);
}
  • Controller, Service, Repository를 위와 같이 구성하였다.
  • 그리고 username을 우리가 필요한 id로 바꾸기 위해 화살표시처럼 코드를 구성하였다.
  • 이렇게 구성하니까 username은 string인데 id 는 Long이어서 다음과 같은 에러가 났다.

    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NumberFormatException: For input string: "dlwjddus16@naver.com"]
    with root causejava.lang.NumberFormatException: For input string: "dlwjddus16@naver.com"

  • String을 Long으로 바꿀 수 없기 때문에 나타나는 에러였다.
  • 그래서 (String) SecurityContextHolder.getContext()...에 있는 String을 Long으로도 바꿔봤는데 동작되지않았다.
  • 애초에 불러오는 username 자체가 String이고 이것을 중간에 바꿀 수는 없는 것 같았다.

방법2

  • 위에 설명했듯이 우리는 토큰에서 username을 얻을 수 있다. 이것은 즉 email 정보이다.

  • 이 값을 가지고 DB 에서 User 정보를 조회해서 필요한 속성을 HttpServletRequest 객체에 추가하는 방법을 이용해보기로했다.

  • 이 기능을 위해서 Custom Filter 생성하고, 생성된 Filer를 SpringSecurity Filter Chain에 추가한다.

  • Custom Filter를 생성해도 되지만, 기존에 있던 JwtVerificationFilter에 통합시켰다.

구조

JwtVerificationFilter

public class JwtVerificationFilter extends OncePerRequestFilter {
    //JWT를 검증하고, Claims(토큰에 포함된 정보)를 얻는데 사용
    private final JwtTokenizer jwtTokenizer;
    //Authentication 객체에 채울 사용자 권한을 생성하는데 이용
    private final CustomAuthorityUtils authorityUtils;

    private final MemberRepository memberRepository;<------추가


    public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, MemberRepository memberRepository) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
        this.memberRepository = memberRepository;<-------추가
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException {
                                 
        try {Map<String, Object> claims = verifyJws(request);//서버에서 전송한 JWT를 request 헤터에서 얻음
            setAuthenticationToContext(claims);//Authentication 객체를 SecurityContext에 저장하기 위한 메서드
        } catch (SignatureException se){
            request.setAttribute("exception", se);
        } catch (ExpiredJwtException ee){
            request.setAttribute("exception", ee);
        } catch (Exception e){
            request.setAttribute("exception", e);
        }
        --------------------------------추가----------
        String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Optional<Member> member = memberRepository.findByEmail(email);
                member.ifPresent(m -> request.setAttribute("memberId", m.getId()));
        ----------------------------------------------
        filterChain.doFilter(request, response);
    }
}

SecurityConfiguration

  • 변경사항을 SecurityConfiguration에 적용시킨다.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration{

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    private final MemberRepository memberRepository;


    public SecurityConfiguration(JwtTokenizer jwtTokenizer,
                                 CustomAuthorityUtils authorityUtils,
                                 MemberRepository memberRepository) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
        this.memberRepository = memberRepository;

    }
    .
    .
    .
    //인증처리 로직 우리가 구현한 JwtAuthenticationFilter 등록 custom filter 등록
    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
        @Override
        public void configure(HttpSecurity builder) throws Exception{
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            
            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            jwtAuthenticationFilter.setFilterProcessesUrl("/trip/login");
            
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
            
            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils, memberRepository);
           
            builder.addFilter(jwtAuthenticationFilter)
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
        }
    }
}
  • 에러메세지는 나오지 않았는데, 회원가입 로직이 작동하지 않았다.
  • 결국에는 원인을 찾을 수 없어서 나중에 알아보기로하고 시간내 완성을 해야했기에 다른방법을 모색할 수 밖에 없었다.

방법3

  • 세번째 방법은 Controller 에서 사용자의 정보를 얻는 방법이다.

  • 이 경우 principal 객체에 직접 접근하여 username을 얻는다.

  • 로그인을 했을때에는 토큰을 가지고있다. 토큰을 가지고있다면, username을 알 수 있게되고 그것을 알면 나머지 정보를 DB에서 조회할 수 있을것이다.

구조

Controller

@GetMapping("/me/questionsTitle")
    public ResponseEntity getMyQuestionsTitle (Principal principal,
                                               @Positive @RequestParam int page,
                                               @Positive @RequestParam int size) {
        Page<Question> questionPage
                = memberFindService.findMyQuestions(principal.getName(), page, size);
        List<Question> questions =  questionPage.getContent();
        List<MemberDto.MemberQuestionResponse> response = mapper.QuestionsToMemberQuestionsResponseDtos(questions);

        return new ResponseEntity<>(
                new MultiResponseDto<>(response, questionPage), HttpStatus.OK);
    }
  • memberFindService.findMyQuestions 에서 username을 통해 id를 조회하고, 페이지네이션을 적용시켰다.
  • 그리고 MemberDto에 MemberQuestionResponse class를 따로 만들어 필요한 정보만 불러오도록 관리하였다.
  • mapper.QuestionsToMemberQuestionsResponseDtos에서는 작성한 질문들을 리스트로 받아와서 변환해주고있다.

MemberFindService

  • Controller에서 principal을 통해 받은 username을 비지니스 로직으로 정제하기위해 따로 클래스를 만들어서 관리했다.
public Page<Question> findMyQuestions(String email, int page, int size){
        Member member = memberService.findMemberByEmail(email);

        return questionService.findMemberQuestions(member.getId(), page-1, size);
    }
  • memberService 의 findMemberByEmail메서드를 통해 email로 member를 불러온 다음
  • member.getId() 를 이용하여 member에서 id를 추출해내고 있다.

QuestionService

public Page<Question> findMemberQuestions(long id, int page, int size) {

        PageRequest pageRequest = PageRequest.of(page, size,
                Sort.by("createdAt").descending());
        return questionRepository.findAllByMemberId(id, pageRequest);
    }
  • 리턴받은 id 값과 page, size를 findMemberQuestions메서드에서 정제하여 해당 id로 작성된 질문들을 questionRepository에서 불러온다.

결과

  • 토큰에서 username을 얻을 수 있게 되었고, 그것을 이용하여 유저의 정보를 선택적으로 추출하여 원하는 결과들을 조회하는 메서드를만들 수 있었다.
profile
반갑습니다.

0개의 댓글