GitHub Repository
Frontend: https://github.com/SihyeonHong/easiest-cv
Backend: https://github.com/SihyeonHong/EasiestCV
localhost:3000/userid 에 접속했을 때 userid가 DB에 존재하면 유저 이름과 그의 CV 홈페이지를, 존재하지 않는 id면 NO SUCH USER 라고 써있는 컴포넌트를 반환하게끔 했다.

/userid/admin)로 이동한다.

UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, String> {
boolean existsById(String userId);
Optional<User> findByUserid(String userId); // JPA 자동생성
}
boolean existsById(String userId); 를 추가했다. 역시 JPA 자동생성되는 메서드이다.
UserController.java
@Operation(summary = "Get user by id", description = "Provide an id to lookup specific user from the user database")
@GetMapping("/userinfo")
public ResponseEntity<?> getUsername(@RequestParam(name="userid") String userid){
try{
return new ResponseEntity<>(userService.getUserById(userid), HttpStatus.OK);
}
catch (NoSuchElementException e){
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
}
}
UserService.java
public Optional<User> getUserById(String userid){
if(userRepository.existsById(userid)){
return userRepository.findByUserid(userid);
} else {
throw new NoSuchElementException("No user found with the provided ID.");
}
}
const [isUserExist, setIsUserExist] = useState(false);
useEffect(() => {
axios
.get(`http://localhost:8080/userinfo?userid=${userid}`)
.then((res) => {
setIsUserExist(true);
console.log(res);
// get additional info
})
.catch((err) => {
setIsUserExist(false);
console.log(err);
});
}, []);
...중략...
<Row>
{isUserExist ? (
activeKey === "home" ? (
<HomeComponent />
) : (
<TabComponent tabname={activeKey} />
)
) : (
<NoUserComponent />
)}
</Row>
JWT는 회원가입 단계가 아니라 로그인 단계에서 사용됨.
저번 팀프로젝트 때 백엔드 담당자가 OAuth를 사용해 로그인 시스템을 구현했던 기억이 나서 처음에는 그걸 사용해 볼 생각이었다.
그런데 좀 더 알아보니 OAuth는 흔히 구글로 로그인, 네이버로 로그인 같은 기능을 쓸 때 많이 사용한다고 한다. 실제로 우리 팀 프로젝트인 TimeCodi에도 구글 캘린더 가져오기, 카카오톡으로 공유하기 기능이 있다.
지금 진행중인 이 프로젝트에서는 소셜 로그인 기능은 필요가 없고, 이 경우에는 JWT를 흔히 사용하는 것 같길래 이걸로 결정했다.
형태는
Header.Payload.Signature
이하의 내용은 colabear754님의 이 블로그 글을 아주 많이 참고했다.
https://gksdudrb922.tistory.com/217 역시도 함께 참고했다.
build.gradle
dependencies {
...
implementation 'io.jsonwebtoken:jjwt:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
...
}
jwt를 적용할 때는 jjwt라는 라이브러리를 사용한다.
src/main/resources/jwt.yml 를 새로 만든다.
secret-key: ONieyFbN1Gqo10bPgUyTFsRMkJpGLXSvGP04eFqj5B30r5TcrtlSXfQ7TndvYjNvfkEKLqILn0j1SmKODO1Yw3JpBBgI3nVPEahqxeY8qbPSFGyzyEVxnl4AQcrnVneI
expiration-hours: 3
issuer: SihyeonHong
/service/JwtTokenProvider.java
위에서 링크한 colabear754님의 TokenProvider.java 코드를 주석까지 그대로 복붙했다.
여기에는 내가 수정한 부분이 전혀 없기 때문에 코드를 글에서 빼고 블로그 링크만 다시 한 번 첨부하겠다.
https://colabear754.tistory.com/171#%EB%A1%9C%EA%B7%B8%EC%9D%B8_API%EB%A5%BC_%ED%98%B8%EC%B6%9C%ED%95%98%EB%A9%B4_%ED%86%A0%ED%81%B0_%EB%B0%9C%EA%B8%89
DTO(Data Transfer Object): 데이터 전송을 위해 사용되는 객체.
/dto/LoginInputData.java
public class LoginInputData {
private String userid;
private String password;
public LoginInputData() {
}
// Getters and Setters ...
로그인 시 사용자에게 입력받은 내용을 담는 객체이다.
/dto/UserDTO.java
public class UserDTO {
private String userid;
private String username;
private String email;
private String img;
private String pdf;
private String intro;
private String token;
public UserDTO(User user, String token){
this.userid = user.getUserid();
this.username = user.getUsername();
this.email = user.getEmail();
this.img = user.getImg();
this.pdf = user.getPdf();
this.intro = user.getIntro();
this.token = token;
}
// getters and setters ...
로그인 성공 시 해당 유저의 정보를 프론트엔드에 넘겨주기 위한 클래스이다.
다음 주에 이름을 LoginOutputDTO라든지 그런 이름으로 리팩토링해야겠다.
원래 LoginInputData가 필요할 줄 모르고 이걸 먼저 만들어서 dto 이름이 이상해졌다.
@Autowired
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider; // 추가
}
...
public UserDTO login(String userid, String password){
User user = userRepository.findByUserid(userid)
.filter(it -> passwordEncoder.matches(password, it.getPassword()))
.orElseThrow(() -> new IllegalArgumentException("Invalid ID or password."));
String token = jwtTokenProvider.createToken(String.format("%s:%s:%s:%s:%s", user.getUserid(), user.getUsername(), user.getEmail(), user.getImg(), user.getPdf()));
return new UserDTO(user, token);
}
해당 유저가 존재하는지 userRepository에서 확인하고,
passwordEncoder.matches(password, it.getPassword()) 를 통해사용자가 입력한 비밀번호가 DB에 암호화되어 저장된 비밀번호와 일치하는지 확인하고,
유저가 존재하지 않거나 비밀번호가 불일치하면 "Invalid ID or password." 메시지와 함께 IllegalArgumentException 에러를 뿜는다.
유저가 존재하면 해당 유저의 정보를 바탕으로 토큰을 생성하고,
토큰과 유저 정보가 담긴 UserDTO 객체를 반환한다.
@Operation(summary = "Log in", description = "Get authentication and go to /userid/admin")
@PostMapping("/login")
public ResponseEntity<?> loginToAdmin(@RequestBody LoginInputData loginData){
try{
UserDTO userDTO = userService.login(loginData.getUserid(), loginData.getPassword());
return new ResponseEntity<>(userDTO, HttpStatus.OK);
} catch (IllegalArgumentException e){
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}
사용자로부터 받은 LoginInputData를 ID와 비밀번호로 분리해 userService의 로그인 기능에 집어넣고, 그 결과를 userDTO와 200 status code와 함께 반환한다.
만약 IllegalArgumentException, 즉 아까 UserService에서 설정한 대로 유저가 존재하지 않거나 비밀번호가 불일치해 에러가 발생한다면 해당 에러의 메시지와 함께 400 status code를 반환한다.
App.js
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<InitPage />} />
<Route path="/:userid" element={<NonAdminHome />} />
<Route
path="/:userid/admin"
element={
<RequireAuth>
<AdminHome />
</RequireAuth>
}
/>
</Routes>
</div>
);
}
...
function LoginForm() {
const [userid, setUserid] = useState("");
const [password, setPassword] = useState("");
const handleLogin = () => {
const data = {
userid: userid,
password: password,
};
console.log(data);
axios
.post("http://localhost:8080/login", data)
.then((res) => {
console.log(res);
sessionStorage.setItem("token", res.data.token);
sessionStorage.setItem("userid", res.data.userid);
// Navigate to the admin page
window.location = `/${userid}/admin`;
})
.catch((err) => {
console.log(err);
alert("Log in failed: " + err.response.data);
});
};
...
로그인 성공 시에는 res.data에 UserDTO와 token이 담겨서 반환된다.
토큰과 유저 아이디를 세션스토리지에 저장한 후 관리자 페이지로 이동시킨다.
관리자 페이지에 들어갈 때는 RequireAuth 컴포넌트로 감싸여 있어서 들어가기 전에 접근 권한을 확인한다.
RequireAuth.js
import { useLocation, Navigate } from "react-router-dom";
function RequireAuth({ children }) {
const token = sessionStorage.getItem("token");
const userid = sessionStorage.getItem("userid");
const location = useLocation();
if (!token || location.pathname.split("/")[1] !== userid) {
return <Navigate to="/" replace />;
}
// if token and userid are valid, render the children
return children;
}
export default RequireAuth;
세션 스토리지에 있는 토큰과 유저 아이디를 확인한다.
토큰이 존재하지 않거나, 해당 유저의 것이 맞는지를 확인하고
권한이 있으면 자식 컴포넌트를 리턴,
권한이 없으면 init page로 돌려보낸다.
우선 User, UserDTO에 String intro 를 추가했다. 이걸 빼먹었더라.
그리고 MySQL 데이터베이스에도 직접 Column을 추가했다.

localhost:3000/{userid} 에 들어가면 제일 먼저 실행되는 get user info 기능에 이 자기소개 데이터가 함께 들어가게 하기 위해서였다. 백엔드가 해당 유저 존재 확인 시 프론트엔드에게 User 정보를 비밀번호 빼고 통째로 넘겨주는 메서드니까.
그리고 프론트엔드는 다음과 같이 수정했다.
NonAdminHome.js
const [isUserExist, setIsUserExist] = useState(false);
const [userinfo, setUserinfo] = useState({}); // userinfo = {username: "", email: "", ...}
...
useEffect(() => {
axios
.get(`http://localhost:8080/userinfo?userid=${userid}`)
.then((res) => {
setIsUserExist(true);
// console.log(res);
setUserinfo(res.data);
// get additional info
// getTabs();
})
.catch((err) => {
setIsUserExist(false);
console.log(err);
});
}, []);
...
<Row>
{isUserExist ? (
activeKey === "home" ? (
<HomeComponent img={userinfo.img} intro={userinfo.intro} />
) : (
<TabComponent tabname={activeKey} />
)
) : (
<NoUserComponent />
)}
</Row>
맨 아랫부분이 핵심.
유저가 존재하면 HomeComponent에 이미지 정보(이건 추가 예정)와 자기소개 정보를 props로 보내주고,
존재하지 않으면 No Such User라고 쓰인 NoUserComponent를 보여준다.
저번 주에는 다른 공부도 하고 개인적인 일정도 많아서 프로젝트에 시간을 거의 못 냈다.
사실 컨디션 조절도 좀 어려웠다. 장마 기간이라 일조량의 영향도 받는 것 같고, 이것도 취준생이라고 나름대로 불안한 모양이다.
결국 이 글도 저번 주에 올렸어야 했는데 미처 정리 못 하고 화요일 되어서야 올리고 있다.
다음 주, 그러니까 이번 주에도 프로젝트는 많이 진도를 못 나갈 것 같다. 어쩌면 한 주 쉬어갈지도.
타입스크립트와 Next.js를 배우기 시작해서 거기에 시간을 좀 많이 쏟을 것 같다.
어쩌면 이 프로젝트에 적용할 수도 있으려나. 그건 배워봐야 알 것 같다.
정말 잘 읽었습니다, 고맙습니다!