본문 바로가기

Spring실습

Spring security + jwt를 이용하여 로그인 구현하기(3)

반응형

이전 글에서 말했던 것과 같이 이번에 spring security + jwt project를 새롭게 리뉴얼하였습니다.

 

https://pinlib.tistory.com/entry/Spring-Security-jwt

 

Spring Security + jwt

이전 글들에서 spring security와 jwt를 이용하여 로그인을 구현하는 실습편들을 작성했었습니다. https://pinlib.tistory.com/entry/Spring-Security-jwt를-이용하여-로그인-구현하기1 Spring Security + jwt를 이용하여

pinlib.tistory.com

 

우선 전과 달라진 code는 매우 많지는 않습니다.

허나, 새로운 project를 만든 이유는 전에 얘기했던 바와 같이 기존에 만들어 놓은 코드들이 spring security설계를 햇갈리게 할 만큼

지저분하게 세팅을 만들었기 때문입니다.

 

//앞으로 spring security + jwt 글을 쓸 때는 두서가 살짝 없을 수 있지만 저의 공부를 위해

//제가 부족한 개념 및 원리에 대한 설명도 함께 작성하겠습니다.

 

우선 SpringSecurityConfig.java 입니다.

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrfConfigurer -> csrfConfigurer.disable())
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests(authorizeRequests -> authorizeRequests
.requestMatchers("member/join", "member/signIn").permitAll()
.requestMatchers("/**").authenticated()
);
http.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}

해당 부분에 대한 변화가 있었습니다.

 

새로운 project를 만들게 되면서 spring boot의 버전이 더 높아지게 되었는데

높아진 버전에서는 기존에 사용하던 .csrf.disable()을 사용할 수 없게 되었습니다.

대신에 lambda식으로 전환하면 사용할 수 있었습니다.

 

또한 requestMatchers에 security적으로 사용 할 url을 넣었습니다.

role에 따른 차별화가 필요한 영역은 아닌 만큼 permitAll()을 넣어 적합한 방식이라면 누구든 이용할 수 있게 만들 었습니다.

 

이제부터는 로그인을 위한 controller와 service code에 대해 이야기 해보겠습니다.

 

우선 MemberController.java 입니다.

package pinlib_studio2.pinlibstudiosecurityspring.controller;

import jakarta.validation.Valid;
import lombok.extern.log4j.Log4j2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import pinlib_studio2.pinlibstudiosecurityspring.data.dto.LoginDTO;
import pinlib_studio2.pinlibstudiosecurityspring.data.dto.MemberDTO;
import pinlib_studio2.pinlibstudiosecurityspring.data.entity.Member;
import pinlib_studio2.pinlibstudiosecurityspring.exception.UserNotFoundException;
import pinlib_studio2.pinlibstudiosecurityspring.security.jwt.TokenUtils;
import pinlib_studio2.pinlibstudiosecurityspring.security.user.UserRole;
import pinlib_studio2.pinlibstudiosecurityspring.service.MemberService;

import static pinlib_studio2.pinlibstudiosecurityspring.security.user.UserRole.ROLE_USER;

@Log4j2
@RestController
public class MemberController {

private Logger logger = LoggerFactory.getLogger(MemberController.class);

private final MemberService memberService;

@Autowired MemberController(MemberService memberService){
this.memberService = memberService;
}

@PostMapping("member/join")
public ResponseEntity<MemberDTO> joinUser(@Valid @RequestBody MemberDTO memberDTO){
String memberId = memberDTO.getMemberId();
String memberPassword = memberDTO.getMemberPassword();
UserRole userRole = ROLE_USER;

MemberDTO response = memberService.joinUser(memberId, memberPassword, userRole);

return ResponseEntity.status(HttpStatus.OK).body(response);
}

@PostMapping("member/signIn")
public ResponseEntity<String> signIn(@Valid @RequestBody LoginDTO loginDTO) {
String loginId = loginDTO.getLogInId();
String loginPassword = loginDTO.getLogInPassword();

String result = memberService.signIn(loginId, loginPassword);
log.info("test log: " + loginId + " " + loginPassword); //null point test

if(result.equals("success")){ //일단 id가 존재한다면
try {
log.info("test success");
//return ResponseEntity.ok("success");
//return ResponseEntity.ok(TokenUtils.generateJwtToken());
Member member = memberService.getUser(loginId);
String token = TokenUtils.generateJwtToken(member); // Generate JWT token
return ResponseEntity.ok(token);
} catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("User not found kkk.");
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
} else {
return ResponseEntity.status(401).body("false");
}
}

@PostMapping("sign/in")
public ResponseEntity<String> signIn2(@Valid @RequestBody LoginDTO loginDTO) {
String loginId = loginDTO.getLogInId();
String loginPassword = loginDTO.getLogInPassword();
log.info("test log: " + loginId + " " + loginPassword);

return ResponseEntity.ok("yes" + loginId + loginPassword);

}

}

기존에는 회원가입과 로그인을 각각 다른 controller에서 관리했었는데 이번에 file정리를 하면서
MemberController.java 한 곳에서 관리할 수 있게 설계하였습니다.

 

우선 joinUser(회원가입) method에서는 role 부분을 추가하였습니다.

role의 경우 enum으로 만들고 admin과 일반 유저인 user로 2개의 영역으로 나누어 설계하였습니다.

 

로그인을 위한 method의 경우 signIn method를 사용합니다.

signIn2의 경우 test용으로 굳이 안만들어도 되는 method입니다.

 

그런데 제가 signIn2 method를 만들면서 log사용법을 익히게 되었습니다.

기존에 android studio를 사용할 때는 log를 정말 잘 이용했었는데

inteliJ에서는 할 줄 몰라서 못사용했었습니다.

 

사용하는 방법은

@Log4j2 annotation을 추가하고 

저의 경우 gradle을 사용하는데 resource안에 (application.properties)가 있는 곳에 log4j2.xml을 추가하였습니다.

후에 signIn2 method에서 사용한 것 처럼 log.info("원하는 내용")으로 사용하시면 됩니다.
저도 솔직히 log4j2.xml의 경우 인터넷에 굴러다니는거 주워 썼으니 쉽게 찾아서 적용하실 수 있을 것입니다.

 

다시 이어서 signIn method에 대하여 이야기 해보자면

 

@PostMapping("member/signIn")
public ResponseEntity<String> signIn(@Valid @RequestBody LoginDTO loginDTO) {
String loginId = loginDTO.getLogInId();
String loginPassword = loginDTO.getLogInPassword();

String result = memberService.signIn(loginId, loginPassword);
log.info("test log: " + loginId + " " + loginPassword); //null point test

if(result.equals("success")){ //일단 id가 존재한다면
try {
log.info("test success");
//return ResponseEntity.ok("success");
//return ResponseEntity.ok(TokenUtils.generateJwtToken());
Member member = memberService.getUser(loginId);
String token = TokenUtils.generateJwtToken(member); // Generate JWT token
return ResponseEntity.ok(token);
} catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("User not found kkk.");
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
} else {
return ResponseEntity.status(401).body("false");
}
}

우선 loginDTO로 id, password값을 client로 부터 받아옵니다.

그 다음에는 memberService의 signIn method를 호출하여 검증하는 절차를 시작합니다.

후에 검증이 성공적으로 완료된다면 memberService의 getUser method를 이용하여 입력받은 id에 해당하는 entity값을 db로 부터 
얻어와서 해당 정보를 바탕으로 token을 발행합니다.

만약 부적절한 인증이라면 미리 만들어 놓은 exception code 들로 예외처리를 해줍니다.

 

이제 MemberServiceImpl.java에 대하여 이야기 해보겠습니다.

 

package pinlib_studio2.pinlibstudiosecurityspring.service.impl;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pinlib_studio2.pinlibstudiosecurityspring.data.dto.LoginDTO;
import pinlib_studio2.pinlibstudiosecurityspring.data.dto.MemberDTO;
import pinlib_studio2.pinlibstudiosecurityspring.data.entity.Member;
import pinlib_studio2.pinlibstudiosecurityspring.data.handler.impl.MemberDataHandler;
import pinlib_studio2.pinlibstudiosecurityspring.data.repository.MemberRepository;
import pinlib_studio2.pinlibstudiosecurityspring.exception.UserNotFoundException;
import pinlib_studio2.pinlibstudiosecurityspring.security.jwt.TokenUtils;
import pinlib_studio2.pinlibstudiosecurityspring.security.user.UserRole;
import pinlib_studio2.pinlibstudiosecurityspring.service.MemberService;

import java.util.Optional;

import static pinlib_studio2.pinlibstudiosecurityspring.security.user.UserRole.ROLE_USER;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
MemberDataHandler memberDataHandler;

@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberDataHandler memberDataHandler){
this.memberDataHandler = memberDataHandler;
}

@Override
public MemberDTO joinUser(String memberId, String memberPassword, UserRole userRole){
Member member = memberDataHandler.joinUserEntity(memberId, passwordEncoder.encode(memberPassword), userRole);
MemberDTO memberDTO = new MemberDTO(member.getMemberId(), member.getMemberPassword(), member.getRole());
return memberDTO;
}

@Override
public Member getUser(String memberId){
Optional<Member> member = memberRepository.findByMemberId(memberId);
return member.get();
}

@Override
public String signIn(String loginId, String loginPassword){
if (isUserIdDuplicated(loginId)) {
// Authenticate the user
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginId, loginPassword)
);

if (authentication.isAuthenticated()) {
return "success";
}
} else {
throw new UserNotFoundException("User not found: " + loginId);
}
return "failure";
}
@Override
public boolean isUserIdDuplicated(String memberId) {
return memberDataHandler.getMemberEntity(memberId);
}



}

우선 전체 코드입니다.

 

기존에 joinUser(회원가입) method를 만들 때와의 차이점이 생겼는데 우선 회원가입시에 role(권한)을 추가하는데

모든 권한 등급을 nomal user등급으로 만들어 줍니다. 

또한 passwordEncoder 부분을 추가하였는데 회원가입할 때 password에 암호화를 걸지 않으면

로그인 할 때 검증 단계에서 정상적으로 작동하지 않습니다.

따라서 회원가입을 할 때에도 password에 BCryptPasswordEncoder를 이용하여 암호화를 해줘야 합니다.

 

이번에는 새롭게 추가하게 된 code인 signIn(로그인) method에 대하여 알아보겠습니다.

@Override
public String signIn(String loginId, String loginPassword){
if (isUserIdDuplicated(loginId)) {
// Authenticate the user
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginId, loginPassword)
);

if (authentication.isAuthenticated()) {
return "success";
}
} else {
throw new UserNotFoundException("User not found: " + loginId);
}
return "failure";
}

해당 코드의 경우 service code에 만들어 놓았던 isUserDuplicated method를 통해 우선적으로 해당 memberId에 해당하는 정보가
db에 존재하는지 확인합니다.

정상적으로 존재하는 경우

우선 provider에 있는 authenticate method를 authenticationManager를 통해 호출하여 검증을 시작합니다.

이에 대한 자세한 내용은 전에 만들어 놓은 게시물인 
https://pinlib.tistory.com/entry/Spring-Security-jwt를-이용하여-로그인-구현하기2

 

Spring Security + jwt를 이용하여 로그인 구현하기(2)

이번시간에는 저번 글에 이어서 SpringAuthenticationFilter.java와 SpringAuthenticationProvider.java 에 대해서 이야기 해보겠습니다. * 이전 글을 읽어보고 오시는 것을 추천드립니다. https://pinlib.tistory.com/entry/S

pinlib.tistory.com

를 보시면 도움이 될 것입니다.

후에 조건문을 통하여 성공적으로 검증이 끝난다면 'success' 라는 string 값을 return해줍니다.

 

만약 존재하지 않는다면 미리 만들어 놓은 exception code를 통해 예외처리를 해줍니다.

 

결과

초기화면에서 일 db에 회원가입을 처리해줍니다.

(first/first) (second/second) 로 id password를 구성했습니다.

 

후에 android studio에서 로그인을 시도해보겠습니다.

 

우선 정상적으로 로그인을 했을 경우입니다. (try -> first/first)

정상적으로 수행되는 모습을 볼 수 있습니다.

 

만약 id는 일치하는데 password가 일치하지 않는 경우를 test해보겠습니다. (try -> first/ trash)

Error response가 뜨며 정상적인 통신이 안되는 모습을 볼 수 있습니다.

 

마지막으로 id가 일치하지 않는(db에 존재하지 않는 id) 경우를 test 해보겠습니다. (try -> trash/ trash)

android studio에서는 위의 test결과와 동일하게 error response가 뜨며 정상적인 통신이 안됩니다.

spring을 실행하는 inteliJ의 경우 미리 만들어 놓은 exception code로 예외처리가 된 모습을 볼 수 있습니다.

 

이렇게 spring security + jwt에 대한 내용은 마무리 하겠습니다.

이제 다음 글들은 2종류로 나뉠 거 같은데 

그동안 spring project를 만들며 조금 더 세부적으로 알아야 할 개념들을 정리하는 게시물(spring 이론편)과

드디어 ios와 aws를 연동하는 과정들을 올릴 것 같습니다.

반응형