앞서 우리는 member 에 관한 entity 들을 만들어 주었습니다.
이 entity 를 이용해서 회원가입을 진행을 하고 해당 정보로
비교를 해서 로그인을 하는 기능을 만들어 보려고 합니다.
우리는 spring security 를 이용해서 로그인 기능을 만들예정입니다.
자체적으로 security는 로그인을 하는 기능을 제공합니다.
하지만 우리는 이것을 그대로 쓸 것은 아니고 커스텀을 하여
어떠한 경로에서의 권한과 usernameandpasswordtoken을 이용한
로그인을 만들어 보려고 합니다.
@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();
}
}
// 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를 받고 있다.