로그인 후 마이페이지에서 내가 쓴 글들을 조회하기위해서는 해당 글들을 누가 썼는지에 대한 사용자의 id가 부여되어있을것이다.
그 id를 직접 API에 노출시켜서 조회 메서드를 만들었었는데, 이번에는 위험을 줄이고자 me로 통일하여 관리하기로 했다.
프로젝트를 진행하며 토큰을 이용하여 member-id를 불러오기위한 방법을 소개하고자 한다.
우리 프로젝트의 마이페이지에는 자신이 쓴글의 Title만 조회하면되기때문에 URI path는 "/me/questionsTitle"로 설정했다.
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(); <-------------------------------------
}
}
private String delegateAccessToken(Member member){
Map<String, Object> claims = new HashMap<>();
claims.put("username", member.getEmail());
claims.put("roles", member.getRoles());
.
.
.
}
간단하게 옮겼지만, 위의 코드를 보면 username에 Email이 들어가고있다.
@GetMapping("/me/questionsTitle")
public ResponseEntity<List<QuestionResponseDto>> getMyQuestionsTitles(){
Long id = Long.parseLong((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();<----------
return ResponseEntity.ok(memberService.getMyQuestionsTitle(id));
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
public interface QuestionRepository entends JpaRepository<Question, Long> {
Page<Question> findByMemberId(Long id);
}
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) SecurityContextHolder.getContext()...
에 있는 String을 Long으로도 바꿔봤는데 동작되지않았다.위에 설명했듯이 우리는 토큰에서 username을 얻을 수 있다. 이것은 즉 email 정보이다.
이 값을 가지고 DB 에서 User 정보를 조회해서 필요한 속성을 HttpServletRequest 객체에 추가하는 방법을 이용해보기로했다.
이 기능을 위해서 Custom Filter 생성하고, 생성된 Filer를 SpringSecurity Filter Chain에 추가한다.
Custom Filter를 생성해도 되지만, 기존에 있던 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);
}
}
@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);
}
}
}
세번째 방법은 Controller 에서 사용자의 정보를 얻는 방법이다.
이 경우 principal 객체에 직접 접근하여 username을 얻는다.
로그인을 했을때에는 토큰을 가지고있다. 토큰을 가지고있다면, username을 알 수 있게되고 그것을 알면 나머지 정보를 DB에서 조회할 수 있을것이다.
@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에서는 작성한 질문들을 리스트로 받아와서 변환해주고있다.
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를 추출해내고 있다.
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에서 불러온다.