회원가입 및 로그인

hyuko·2023년 4월 29일
0

Spring-Study

목록 보기
7/8

회원가입 / 로그인

앞서 우리는 member 에 관한 entity 들을 만들어 주었습니다.
이 entity 를 이용해서 회원가입을 진행을 하고 해당 정보로
비교를 해서 로그인을 하는 기능을 만들어 보려고 합니다.


우리는 spring security 를 이용해서 로그인 기능을 만들예정입니다.
자체적으로 security는 로그인을 하는 기능을 제공합니다.

하지만 우리는 이것을 그대로 쓸 것은 아니고 커스텀을 하여
어떠한 경로에서의 권한과 usernameandpasswordtoken을 이용한
로그인을 만들어 보려고 합니다.


Controller

  • 컨트롤러는 클라이언트와의 통신을 담당하고 클라이언트 측에서 오는 request 요청을
    받아서 컨트롤 말그대로 컨트롤해주는 역할을 합니다.
    이 때 우리는 server side randering 이 아닌 클라이언트 사이드 랜더링을 이용할 예정이기 때문에 restcontroller를 사용할 예정입니다.

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authenticationService;

	// VallidAspect 라는 어노테이션은 우리가 임의적으로 custom한 어노테이션입니다.
    // 해당 어노테이션이 붙은 컨트롤러들은 valid 체크를 하고 aop 로 만든 advice controller 로 넘어가게 됩니다.

    @ValidAspect
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody @Valid LoginReqDto loginReqDto, BindingResult bindingResult){
        return ResponseEntity.ok(authenticationService.login(loginReqDto));
    }

    @ValidAspect
    @PostMapping("/signup")
    public ResponseEntity<?> signup(@RequestBody @Valid SignupReqDto signupReqDto, BindingResult bindingResult) {
        authenticationService.duplicatedEmailCheck(signupReqDto.getEmail());
        authenticationService.save(signupReqDto);
        return ResponseEntity.ok(true);
    }

    @GetMapping("/authenticated")
    public ResponseEntity<?> authenticated(String accessToken) {
        return ResponseEntity.ok(authenticationService.authenticated(accessToken));
    }

    @GetMapping("/principal")
    public ResponseEntity<?> principal(String accessToken) {

        return ResponseEntity.ok(authenticationService.getPrincipal(accessToken));
    }


}

회원가입을 하는 컨트롤러로는 매핑 주소가 /signup 인 곳으로 요청이 들어오며
로그인을 할 때 jwt 토큰을 생성하여 발급해주면서 토큰을 이용한 로그인으로 진행합니다.


회원가입 서비스

우리는 회원가입시에 일반회원이라고 무조건 가정하에 role을 ROLE_USER로 부여하도록 합니다.
그리고 회원가입을 할 때 DB에 저장될 당시에는 무조건 암호화가 되어서 저장되어야합니다.
회원가입후 로그인을 할 때는 다시 복호화 하여서 비교하여야합니다.

그리고 회원당 권한을 가지는 authorities 즉 복수인 이유는 여러가지의 권한을 받을 수 있기에 List 형태로 받아오고 authorities는 role을 포함하고 있습니다.

member와 role을 1:1로 바로 연결하는 것이아니라 authority라는 권한이라는 테이블을 중간에 두고 이 것을 통해 1 : N 그리고 N : 1로 풀어서 해결하도록 합니다.


// spring boot 에서 context에 등록을 위해서 component를 등록 해주어야 합니다.
// 여기서 의문점이 들텐데 component를 등록하라고 했는데 앞의 controller도 마찬가지로 component가 보이지 않는데 그 이유는 Service라는 어노테이션과 RestController는 컴포넌트를 포함하고 있습니다.
// 그렇기 때문에 명시적으로 더욱 더 명확하게 Service 와 RestController를 붙여 줌으로써 역할이 명확하게 표시됩니다.

@Service
@RequiredArgsConstructor
public class AuthenticationService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final AuthenticationManagerBuilder authenticationManager;
    private final JwtTokenProvider jwtTokenProvider;

// 메소드명만 보더라도 알 수 있도록 만들어 줍니다. 우리는 email을 username 즉 
// 로그인시 사용하는 username으로 쓸 예정이기 때문에 해당 email이 중복인지를 체크해줍니다.
    public void duplicatedEmailCheck(String email) {
        if (memberRepository.findMemberByEmail(email) != null) {

            throw new CustomException("duplicated error",
                    ErrorMap.builder()
                    .put("email", "이미 존재하는 이메일입니다.")
                    .build());
        }
    }

// save 회원가입에 관한 로직입니다.
    public void save(SignupReqDto signupReqDto) {
        Member memberEntity = signupReqDto.toEntity();
        memberRepository.save(memberEntity);


        memberRepository.saveAuthority(Authority.builder()
                                    .memberId(memberEntity.getMemberId())
                                    .roleId(1)
                                    .build());
    }

    public JwtTokenRespDto login(LoginReqDto loginReqDto) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginReqDto.getEmail(), loginReqDto.getPassword());

        Authentication authentication = authenticationManager.getObject().authenticate(authenticationToken);

        return jwtTokenProvider.createToken(authentication);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member memberEntity = memberRepository.findMemberByEmail(username);
        if (memberEntity == null) {
            throw new CustomException("Login failed",ErrorMap.builder().put("email", "존재하지 않는 사용자입니다.").put("password", "존재하지 않는 사용자입니다.").build());
        }
        return memberEntity.toPrincipal();
    }

    public boolean authenticated(String accessToken) {

        return jwtTokenProvider.validateToken(jwtTokenProvider.getToken(accessToken));
    }

    public PrincipalRespDto getPrincipal(String accessToken) {
        Claims claims = jwtTokenProvider.getClaims(jwtTokenProvider.getToken(accessToken));
        Member memberEntity = memberRepository.findMemberByEmail(claims.getSubject());

        return PrincipalRespDto.builder()
                .memberId(memberEntity.getMemberId())
                .email(memberEntity.getEmail())
                .name(memberEntity.getName())
                .authorities((String) claims.get("auth"))
                .build();
    }
}


Repository 와 DB Mapping query


// mybatis 를 이용하는 방법 중 우린 xml 을이용할 예정이고
// xml 과 연결하기위해 @Mapper를 붙여줍니다

@Mapper
public interface MemberRepository {

    Member findMemberByEmail(String email);
    void save(Member member);
    void saveAuthority(Authority authority);

}

해당 메소드들에 대한 xml 쿼리를 알아보도록 하겠습니다.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.toyproject.bookmanagement.repository.MemberRepository">

    <resultMap id="memberMap" type="com.toyproject.bookmanagement.domain.entity.Member">
        <id property="memberId" column="member_id"/>
        <result property="email" column="email"/>
        <result property="password" column="password"/>
        <result property="name" column="name"/>
        <result property="provider" column="provider"/>

        <collection property="authorities" javaType="list" resultMap="authorityMap"/>
    </resultMap>

    <resultMap id="authorityMap" type="com.toyproject.bookmanagement.domain.entity.Authority">
        <id property="authorityId" column="authority_id"/>
        <result property="memberId" column="member_id"/>
        <result property="roleId" column="role_id"/>
        <association property="role" resultMap="roleMap"/>
    </resultMap>

    <resultMap id="roleMap" type="com.toyproject.bookmanagement.domain.entity.Role">
        <id property="roleId" column="role_id"/>
        <result property="roleName" column="role_name"/>
    </resultMap>


    <select id="findMemberByEmail" resultMap="memberMap">
        SELECT
            mt.member_id,
            mt.email,
            mt.password,
            mt.name,
            mt.provider,

            at.authority_id,
            at.member_id,
            at.role_id,

            rt.role_id,
            rt.role_name
        FROM
            member_tb mt
            LEFT OUTER JOIN authority_tb at ON(at.member_id = mt.member_id)
            LEFT OUTER JOIN role_tb rt ON(rt.role_id = at.role_id)
        WHERE
            mt.email = #{email}
    </select>

    <insert id="save"
            parameterType="com.toyproject.bookmanagement.domain.entity.Member"
            useGeneratedKeys="true"
            keyProperty="memberId"
    >
        INSERT INTO
            member_tb
        VALUES
            (0, #{email}, #{password}, #{name}, #{provider})
    </insert>

    <insert id="saveAuthority" parameterType="com.toyproject.bookmanagement.domain.entity.Authority">
        INSERT INTO
            authority_tb
        VALUES
            (0, #{memberId}, #{roleId})

    </insert>
</mapper>

연관관계 매핑을 하기위해서 resultMap 을이용합니다.
각각의 객체를 resultMap 으로 만든후에 property에는 만든 객체에 대한
변수들 그리고 column 에는 DB에 정해져있는 column명을 적어줍니다.

그리고 collection은 리스트형식의 변수들을 담을 수 있고
그냥 객체만 받아올 때는 association을 이용해서 받아올 수 있다.

이전에 말한 것과 같이 1 : N 일경우 1인 곳이 중심이 되어 N을 포함하고 있는다.
그렇기 때문에 collection으로 N인 authorities를 받고 있다.

profile
백엔드 개발자 준비중

0개의 댓글