로그인을 하기 위해 JWT 토큰을 사용하기로 결정 했습니다.
그래서 그전에 JWT 토큰이 무엇인지 어떻게 사용되는 것인지 먼저 간단하게 알아보려고 합니다.
JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다. 그리고 JWT 기반 인증은 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식입니다.
JWT는 JSON 데이터를 Base64 URL-safe Encode 를 통해 인코딩하여 직렬화한 것이며, 토큰 내부에는 위변조 방지를 위해 개인키를 통한 전자서명도 들어있다. 따라서 사용자가 JWT 를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며 검증이 완료되면 요청한 응답을 돌려줍니다.
JWT는 . 을 구분자로 나누어지는 세 가지 문자열의 조합이다.
. 을 기준으로 좌측부터 Header, Payload, Signature를 의미합니다.
Header 에는 JWT 에서 사용할 타입과 해시 알고리즘의 종류가 담겨있으며, Payload 는 서버에서 첨부한 사용자 권한 정보와 데이터가 담겨있다. 마지막으로 Signature 에는 Header, Payload 를 Base64 URL-safe Encode 를 한 이후 Header 에 명시된 해시함수를 적용하고, 개인키(Private Key)로 서명한 전자서명이 담겨있습니다.
그럼 이제 Spring Secuity를 사용하여 JWT 토큰을 이용한 자체 로그인을 해보도록 하겠습니다.
build.gradle
// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
JWT 사용을 위해 gradle에 추가합니다.
Spring Security 사용자 인증 및 권한 부여를 위해 UserDetails를 상속받은 CustomUserDetail과 UserDetailsService를 상속받은 CustomUserDetailService를 만들어줍니다.
CustomUserDetails.java
@Getter
@NoArgsConstructor
public class CustomUserDetails implements UserDetails {
private Long userId;
private String email;
private String password;
private String role;
@Builder
public CustomUserDetails(Long userId, String email, String password, String role) {
this.userId = userId;
this.email = email;
this.password = password;
this.role = role;
}
public Long getUserId() {
return userId;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(Role.USER.getKey()));
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
CustomUserDeatilsService.java
@Primary
@RequiredArgsConstructor
@Service
@Transactional
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
return CustomUserDetails.builder()
.userId(user.getId())
.email(user.getEmail())
.password(user.getPassword())
.role(user.getRoleKey())
.build();
}
}
Login 도메인을 따로 생성하여 OneToOne 관계로 맵핑하려고 합니다.
Login.java
@Getter
@NoArgsConstructor
@Entity
public class Login extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OnDelete(action = OnDeleteAction.CASCADE)
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@Column
private String refreshToken;
@Builder
public Login(User user, String refreshToken) {
this.user = user;
this.refreshToken = refreshToken;
}
// 리프레쉬 토큰 업데이트
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
LoginRepository.java
public interface LoginRepository extends JpaRepository<Login, Long> {
Optional<Login> findByRefreshToken(String refreshToken);
Optional<Login> findByUserId(Long userId);
}
먼저 로그인 Controller를 작성하도록 하겠습니다.
앞선 글과 같이 반환 값을 맞추기 위하여 CommonResponse와 ResultDto를 사용했습니다.
LoginController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/all")
@Api(tags = "Auth APIs")
public class LoginController {
private final BasicLoginService basicLoginService;
private final OAuth2LoginService oAuth2LoginService;
@ApiOperation(value = "자체 로그인 API", notes = "자체 로그인 진행")
@PostMapping("/login/basic")
public ResponseEntity<ResultDto<Void>> basicLogin(@RequestBody BasicLoginRequestDto basicLoginRequestDto, HttpServletResponse httpServletResponse) {
CommonResponse<Object> commonResponse = basicLoginService.basicLoginResponse(basicLoginRequestDto, httpServletResponse);
ResultDto<Void> result = ResultDto.in(commonResponse.getStatus(), commonResponse.getMessage());
return ResponseEntity.status(commonResponse.getHttpStatus()).body(result);
}
}
로그인을 위한 email과 password 정보를 가져오는 Dto도 만들어주겠습니다.
BasicLoginRequestDto.java
@Getter
@NoArgsConstructor
public class BasicLoginRequestDto {
@ApiModelProperty(name = "email", value = "login email", example = "abcd@naver.com")
private String email;
@ApiModelProperty(name = "password", value = "login password", example = "abcd1234")
private String password;
@Builder
public BasicLoginRequestDto(String email, String password) {
this.email = email;
this.password = password;
}
public Login toEntity(User user, String refreshToken) {
return Login.builder()
.user(user)
.refreshToken(refreshToken)
.build();
}
}
로그인 서비스 부분입니다.
BasicLoginService.java
@RequiredArgsConstructor
@Service
public class BasicLoginService {
private final UserRepository userRepository;
private final LoginRepository loginRepository;
private final JwtProvider jwtProvider;
private final AuthenticationManager authenticationManager;
private final CommonService commonService;
// 자체 로그인
@Transactional
public GeneratedTokenDto basicLogin(BasicLoginRequestDto basicLoginRequestDto) {
String requestEmail = basicLoginRequestDto.getEmail();
String requestPassword = basicLoginRequestDto.getPassword();
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(requestEmail, requestPassword));
SecurityContextHolder.getContext().setAuthentication(authentication);
User user = userRepository.findByEmail(basicLoginRequestDto.getEmail())
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
String accessToken = jwtProvider.generateAccessToken(user.getEmail(), user.getRoleKey());
String refreshToken = jwtProvider.generateRefreshToken();
Login login = basicLoginRequestDto.toEntity(user, refreshToken);
loginRepository.save(login);
return GeneratedTokenDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
} catch (Exception e) {
throw new LoginException(ErrorCode.LOGIN_EXCEPTION);
}
}
// API 반환
@Transactional
public CommonResponse<Object> basicLoginResponse(BasicLoginRequestDto basicLoginRequestDto, HttpServletResponse httpServletResponse) {
GeneratedTokenDto generatedTokenDto = basicLogin(basicLoginRequestDto);
httpServletResponse.setHeader("Authorization", generatedTokenDto.getAccessToken());
httpServletResponse.setHeader("REFRESH-TOKEN", generatedTokenDto.getRefreshToken());
return commonService.successResponse(SuccessCode.BASIC_LOGIN_SUCCESS.getDescription(), HttpStatus.CREATED, null);
}
}
다음과 같은 과정을 거칩니다.
- AuthenticationManager를 사용한 인증:
- AuthenticationManager를 사용하여 사용자가 입력한 이메일과 비밀번호를 이용하여 사용자를 인증합니다.
- UsernamePasswordAuthenticationToken을 사용하여 이메일과 비밀번호를 담은 토큰을 생성하고, AuthenticationManager를 통해 이를 검증하고 사용자를 인증합니다.
- 사용자 검색:
- userRepository를 사용하여 주어진 이메일을 가진 사용자를 데이터베이스에서 검색합니다.
- 만약 사용자가 존재하지 않으면 NotFoundException을 발생시킵니다.
- JWT 토큰 생성:
- jwtProvider를 사용하여 접근 토큰과 리프레시 토큰을 생성합니다.
- 접근 토큰은 사용자의 이메일과 역할 (Role) 정보를 기반으로 생성됩니다.
- 리프레시 토큰은 간단하게 생성됩니다.
- 로그인 정보 저장:
- basicLoginRequestDto를 사용하여 Login 엔티티를 생성하고, 리프레시 토큰을 저장합니다.
- loginRepository를 사용하여 생성된 Login 엔티티를 데이터베이스에 저장합니다.
- 토큰 반환:
- 생성된 접근 토큰과 리프레시 토큰을 GeneratedTokenDto 객체에 담아 반환합니다.
그 후 API 반환에서 header에 토큰을 담아 반환합니다.
반환을 위한 GeneratedTokenDto를 생성합니다.
GeneratedTokenDto.java
@Getter
@NoArgsConstructor
public class GeneratedTokenDto {
private String accessToken;
private String refreshToken;
private static final String TOKEN_PREFIX = "Bearer ";
@Builder
public GeneratedTokenDto(String accessToken, String refreshToken) {
this.accessToken = TOKEN_PREFIX + accessToken;
this.refreshToken = TOKEN_PREFIX + refreshToken;
}
}
다음 글에서는 Security 설정과 Token 설정에 대해 작성하도록 하겠습니다.
[Spring Boot]회원 가입 & 로그인(2)
이번엔 도메인 설정 이후 컨트롤러와 서비스를 만들어 보도록 하겠습니다. 먼저 반환 형식을 맞추려고 합니다. Response { "status" : "success", "message" : "회원 가입 성공", "data" : { "userId" : 1 } } 위와 같
classruntime.tistory.com
[Spring Boot]회원 가입 & 로그인 (1)
기본 회원 가입 및 로그인을 구현하면서 소셜 로그인도 같이 병합해서 사용해야겠다는 생각이 들어 만들어 봤습니다. ERD는 쇼핑몰을 생각하면서 만들어 봤습니다. 먼저 기본 회원 가입 ERD 입니
classruntime.tistory.com
GitHub - Llimy1/Auth_Spring
Contribute to Llimy1/Auth_Spring development by creating an account on GitHub.
github.com
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] 회원 가입 & 로그인(5) (0) | 2023.10.04 |
---|---|
[Spring Boot] 회원 가입 & 로그인(4) (1) | 2023.10.04 |
[Spring Boot]회원 가입 & 로그인(2) (0) | 2023.09.26 |
[Spring Boot]회원 가입 & 로그인 (1) (0) | 2023.09.26 |