네이버 클라우드 캠프/Spring Boot & React

[SpringBoot & React] 회원가입과 로그인(2)

graph-dev 2023. 7. 31. 20:25
728x90

boot and react logo

 

이번에는 로그인을 해보겠습니다.

로그인

로그인과 관련된 메서드를 Service 인터페이스에 먼저 정의합니다. 아이디와 비밀번호가 필요하겠죠?

SpringBoot

Service

public interface MemberService {

    Member login(String username, String password);
}

 

ServiceImplement

그리고 이 login 메서드에 대한 구현부를 정의합니다. 이 RequiredArgsConstructor를 사용하면 레포지토리와 인코더 등의 객체를 별도로 의존성 주입 없이 final로 선언해서 사용할 수 있다고 합니다. 유용합니다.

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Member login(String username, String password) {
        //username으로 조회
        Optional<Member> loginMember = memberRepository.findByUsername(username);

        if(loginMember.isEmpty()) {
            throw new RuntimeException("not exist username");
        }

        //비밀번호 비교
        if(passwordEncoder.matches(password, loginMember.get().getPassword())){
            throw new RuntimeException("wrong password");
        }

        return loginMember.get();
    }
}

 

 코드를 잠깐 살펴보면 사용자 아이디를 입력해서 username이 데이터베이스에 없으면 not exist username 메시지를 반환하고, 비밀번호는 matches 메서드를 사용합니다.

 PasswordEncoder의 matches객체에 암호화되지 않은 비밀번호, 암호화된 비밀번호 순으로 파라미터를 입력해주면 실제 비밀번호와 비교해서 올바르지 않으면 wrong password로 메시지를 줍니다. wrong password를 제공하려면, 이 암호화된 비밀번호를 비교하기 위해 풀어주는 작업이 필요하겠죠? 데이터베이스에 있는 암호화된 비밀번호와 비교를 위해서는 이러한 PasswordEncoder 객체를 사용해줍시다. 이 findByusername 메서드가 궁금하죠? 살펴보겠습니다.

 

Repository

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByUsername(String username);
}

 

레포지토리에는 username을 기초로 데이터베이스에서 멤버를 찾을 수 있습니다. 엔티티를 잠깐 살펴보겠습니다. id, username, password, role 컬럼을 기초로 하고 작성했습니다.

Entity

@Entity
@Table(name="T_MEMBER",
        //username UK로 지정
        uniqueConstraints = {@UniqueConstraint(columnNames = "username")}
      )
@SequenceGenerator(
        name="MemberSeqGenerator",
        sequenceName = "T_MEMBER_SEQ",
        initialValue = 1,
        allocationSize = 1
)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "MemberSeqGenerator"
    )
    private long id;
    @Column(nullable = false)
    private String username;
    @Column(nullable = false)
    private String password;
    @ColumnDefault("'ROLE_USER'")
    private String role;

    public MemberDTO toMemberDTO() {
        return MemberDTO.builder()
                .id(this.id)
                .username(this.username)
                .password(this.password)
                .role(this.role)
                .build();
    }
}

 

이렇게 완성하고 React로 넘어가서 코드를 살펴보겠습니다.

 

REACT

로그인 함수를 정의해보겠습니다. useNavigate 메서드로 경로가 / 인 메인 화면으로 이동시킬 수 있습니다. 그리고 username, password는 인풋 태그의 값을 받아서 로그인을 수행하도록 합니다. 특히, 로그인할 때는 axios라는 패키지를 활용하는데, post 메서드를 통해 username, password값을 작성해서 서버로 보냅니다. 그대로 보내지 않고 토큰 값을 저장합니다.

const Login = () => {
    const navi = useNavigate();

    const onSubmit = useCallback((e) => {
        e.preventDefault();

        const data = new FormData(e.target);
        const username = data.get("username");
        const password = data.get("password");

        login(username, password);
    }, []);

    const login = useCallback(async (username, password) => {
        try {
            const response = await axios.post('http://localhost:9090/api/member/login', 
            {username: username, password: password});

            if(response.data && response.data.item.token !== null && response.data.item.token !== "") {
                localStorage.setItem("ACCESS_TOKEN", response.data.item.token);
                navi("/")
            }

            console.log(response);

        } catch(e) {
            alert(e.response.data.errorMessage);
        }
    }, []);

 

화면 부는 return 이하에서 아래와 같이 정의합니다.

return (
    <Container component="main" maxWidth="xs" style={{marginTop: "8%"}}> 
        <form onSubmit={onSubmit}>
            <Grid container spacing={2}>
                <Grid item xs={12}>
                    <Typography component="h1" variant="h5">
                        로그인
                    </Typography>
                </Grid>
                <Grid item xs={12}>
                    <TextField
                        name="username"
                        variant="outlined"
                        required
                        fullWidth
                        id='username'
                        label="아이디"
                        autoFocus
                    ></TextField>
                </Grid>
                <Grid item xs={12}>
                    <TextField
                        name="password"
                        variant="outlined"
                        required
                        fullWidth
                        id='password'
                        label="비밀번호"
                        type="password"
                    ></TextField>
                </Grid>
                <Grid item xs={12}>
                    <Button type='submit' fullWidth variant='contained'
                    color='primary'>로그인</Button>
                </Grid>
            </Grid>
            <Grid container justifyContent="flex-end">
                <Grid item>
                    <Link href='/join' variant='body2'>
                        계정이 없으시면 여기서 회원가입하세요.
                    </Link>
                </Grid>
            </Grid>
        </form>
    </Container>
  )

마지막으로 AppRoute를 작성해줍니다. useNavigate에서 이 path를 활용해서 쉽게 설정할 수 있습니다.

const AppRouter = () => {
  return (
    <Routes>
        <Route path='/' element={<App></App>}></Route>
        <Route path='/join' element={<Join></Join>}></Route>
        <Route path='/login' element={<Login></Login>}></Route>
    </Routes>
  );
};

 

 

그렇게 화면이 아래와 같이 완성됩니다.

로그인