TIL ] 스프링+JPA+시큐리티+타임리프+Redis 로 JWT인증 구현하기-2

BRINCE·2022년 11월 8일
1

해당 글들의 코드는 제가 작성했던 코드의 모든 부분을 가져온게 아니라 단순 저의 복습용으로 몇가지 추가된 기능들이 삭제된 코드들이라 제대로 동작이 안될수도 있습니다.

회원가입까지 구현해놨으면 뷰를 꾸며보자

컨트롤러로 url 입력시에 이동이 되도록 구현해놨으면, 이제 뷰를 꾸밀 차례다.

백엔드를 공부하면서 난관에 부딪혔던 때가 바로 이때였다. 나는 프론트를 하나도 못하는데..... 단순히 html 살짝 배웠던게 끝이었다.

이러한 우리를 구원해줄것이 있는데 그건 바로 타임리프와 부트스트랩이다.

타임리프는 스프링에서 공식적으로 지원하는 뷰 템플릿인데, 스프링에서도 타임리프를 사용할것을 권장할 정도로 호환성이 굉장히 좋다.

문법 자체도 크게 어려운게 없어 바로바로 이해하기 쉬운 수준이다.
거기에 부트스트랩 또한 나처럼 프론트부분을 하나도 모르는 자들을 구원해주는 프레임워크 인데,

적용하고 싶은 테마가 있다면 단순히 해당 테마를 배포하는 사이트에서 스크립트를 긁어와서 갖다 붙이기만 하고 나는 폼같은 태그만 추가해주면 끝이다.

타임리프 시작하기

일단 build.gradle 에 타임리프 디펜던시를 추가해준다.
지금 당장은 헤더와 푸터를 신경쓸 부분이 아니니 해당 뷰만 작성하는걸 목표로 하자.

타임리프는 단순히 포스트맵핑으로 정해둔 주소를 div 태그에 집어넣고, 각각의 폼 태그에 비즈니스 로직에서 받아올 dto의 각각 이름을 id로만 적어줘도 해당값을 서버로 전달해준다. (따로 request용 dto만 생성해서 전달해주면 된다.)

단순히 내가 정보 전달을 위해 사용하는 dto는 되도록이면 하나로 퉁치치말고 각각의 요청에 공통적으로 전달할수 있는 부분만 쏙 빼서 따로 생성하자. (퉁쳐서 전달하면 불필요한 정보를 너무 많이 주고받게 된다.)

회원가입 같은 경우에는 모든 정보가 필요하니 기존에 생성해둔 UserAccountDto 를 그대로 사용했다.

html 작성하기

일단 내가 전달해야 하는 정보는 아이디, 비밀번호이다. (사실은 이메일 닉네임 자기소개 이렇게 3개가 더 있지만, 일단은 기능 구현이 목표이니 아이디 비밀번호만 입력받는쪽으로 해보자.)

단순히 타임리프는 따로 하나하나 뭘 지정해줄 필요 없이 th:action 이라는 문법으로 내가 미리 선언해둔 컨트롤러 메소드를 동작시키기 위한 태그임을 지정해준다.

<form th:action="@{/signup}" method="post" style="width:700px">
  <p class="register">회원가입</p>
  <label for="userId" class="form-label"> 아이디 </label>
    <input id="userId" type="text" class="input">
   <label for="password1" class="form-label"> 비밀번호 </label>
    <input id="password1" type="password" class="input">
  <button type="submit">가입</button>
  &nbsp;
  &nbsp;
  <button th:action="@{/articles}" type="button">취소</button>
</form>

이렇게 처음 폼 태그에 th:action 으로 내가 실행할 rest api 메소드 주소와 어떠한 역할을 하는지 method 에 넣어준다.
input 태그에 들어갈 id 는 꼭 내가 전달할 Dto 의 필드에 선언되어있는 변수명과 같아야한다.

폼 태그 안에 있는 submit 을 위한 버튼 태그에는 따로 url 을 입력해줄 필요가 없다.

가입하고 로그인 해보기

전 글에 설명했듯이, 스프링 시큐리티는 설정을 해주면 따로 내가 로그인폼을 작성하지 않아도 로그인 폼을 제공하기 때문에, 회원가입 폼만 작성하고 가입이 완료됨을 확인한다면 로그인까지 진행할 수 있다.

그전에 따로 먼저 작성해줘야 할 것들이 있다.

바로 스프링 시큐리티의 필터중에 UsernamePasswordAuthentiactionFilter 라는것이 있는데, 해당 필터가 동작하면 마지막 절차로 UserDeatilsService 에서 인증 작업을 마무리한다.

내가 단순히 아무것도 하지 않고 끝내면, 스프링 시큐리티는 로그인을 어떤 데이터를 기반으로 처리해줘야할지 모르기때문에, 해당 인터페이스들을 구현해서 내가 직접 따로 작성해줘야한다.

UserDetails, UserDeatilsService 구현한 클래스 작성하기

일단 UserDetailsService의 loadUserByUsername 라는 메소드를 내가 미리 만들어둔 레포지토리에서 데이터를 가져오도록 재정의 해줘야 로그인이 가능해지기 때문에 먼저 클래스를 작성해주자.

@RequiredArgsConstructor
@Transactional
@Service
public class UserSecurityService implements UserDetailsService {
    private final UserAccountRepository userAccountRepository;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
            Optional<UserAccount> _account = userAccountRepository.findById(userId);
            if (_account.isEmpty()) {
                throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
            }
            UserAccount account = _account.get();
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(account.getRole());
return new BoardPrincipal(account.getUserId(), account.getUserPassword(), authorities;
        }
}

이렇게 내 프로젝트 전용으로 작성해준 계정 엔티티를 임시로 불러와 해당 계정이 존재한다면
해당 계정정보를 리턴해주고, 스프링 시큐리티는 반환값을 토대로 인증을 진행한다.

그럼 여기서 보이는 Principal Dto를 또 만들어주자.

PrincipalDto 는 UserDeatils 를 구현해 작성한다.

public record BoardPrincipal(
        String username,
        String password,
        Collection<? extends GrantedAuthority> authorities,
        String email,
        String nickname,
        String memo
)  implements UserDetails {
    public static BoardPrincipal of(String username, String password, String email, String nickname, String memo) {
        // 지금은 인증만 하고 권한을 다루고 있지 않아서 임의로 세팅한다.
        Set<UserAccountRole> roleTypes = Set.of(UserAccountRole.USER);

        return new BoardPrincipal(
                username,
                password,
                roleTypes.stream()
                        .map(UserAccountRole::getValue)
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toUnmodifiableSet()),
                email,
                nickname,
                memo
        );
    }
    public static BoardPrincipal from(UserAccountDto dto) {
        return BoardPrincipal.of(
                dto.userId(),
                dto.userPassword(),
                dto.email(),
                dto.nickname(),
                dto.memo()
        );
    }

    public UserAccountDto toDto() {
        return UserAccountDto.of(username,password, email, nickname, memo);
    }
    @Override public String getUsername() { return username; }
    @Override public String getPassword() { return password; }
    @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }

    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }

}

UserDeatils를 구현해 따로 UserDetails 를 건내주고 받아올때 직접 userdetails 가 아닌 중간 Dto를 전달하게 만들어줬다.
입맛대로 본인이 만들어둔 컬럼값들을 집어넣어도 된다.

그리고 UserDetailsService 를 구현한 서비스클래스를 따로 작성해준다.
(따로 계정관련 서비스 클래스에 직접 구현하거나, 유저 엔티티에 바로 details 를 구현해 작성할 수 있지만, 높은 응집도와 낮은 결합도(?) 를 위해 따로 작성해주었다.)

@RequiredArgsConstructor
@Transactional
@Service
public class UserSecurityService implements UserDetailsService {
    private final UserAccountRepository userAccountRepository;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
            Optional<UserAccount> _account = userAccountRepository.findById(userId);
            if (_account.isEmpty()) {
                throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
            }
            UserAccount account = _account.get();
            List<GrantedAuthority> authorities = new ArrayList<>();
             authorities.add(new SimpleGrantedAuthority(account.getRole.getValue()));
        return new BoardPrincipal(account.getUserId(), account.getUserPassword(), authorities, account.getEmail(), account.getNickname(), account.getMemo());
        }
}

이렇게 UserDetailsService 를 구현하면, 해당 인터페이스가 가진 메소드를 재정의 할 수 있게 된다.

이때 기존에 작성했던 계정관련 엔티티와 연결시켜 메소드를 작성하면, 해당 메소드가 유저정보를 userDetails 형식을 넘겨줘서 그걸로 인한 인증처리를 하게 된다.
(계정 생성시에 권한은 자동으로 엔티티에 선언해두었던 USER권한으로 잡히게 된다.)

로그인 해보기


(내 코드는 유저, 관리자 권한을 둘다 가지게 설정되있어서 어드민 페이지까지 뜬다.)

이 경우에 스프링 시큐리티가 로그아웃까지 지원해주기때문에, GetMapping 으로 로그아웃 url 을 설정한후에 타임리프로 로그아웃 버튼에 해당 url을 링크로 걸어두면, 버튼을 클릭하면 로그아웃이 된다.

다음 글에선 본격적으로 만들어놓은 틀을 가지고 Jwt 토큰을 이용한 인증방식으로 로그인방식을 변경해보자 .

profile
자스코드훔쳐보는변태

0개의 댓글