개발

[Spring Boot] jwt로 로그인, 회원가입 API 구현

changha. 2023. 7. 7. 17:57

이전 게시글에서 간단히 loginDto에 username을 입력하면 token을 발급하는 부분을 구현해봤습니다! 

https://changha-dev.tistory.com/160

 

[Spring Boot] Security + jwt로 인증, 인가 구현하기

스프링 시큐리티에서 JWT 토큰 인증 방식을 사용하기 위해서는 다음과 같은 과정을 거칩니다: 사용자 로그인: 클라이언트에서 사용자의 로그인 정보(예: 아이디와 비밀번호)를 서버에 전송합니

changha-dev.tistory.com

 

이번에는 이어서 회원가입과 로그인 부분을 추가로 구현해보겠습니다. 

 


build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.13'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
	maven {
		url 'https://repo.clojars.org'
		name 'Clojars'
	}
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	runtimeOnly 'com.h2database:h2'




	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'


}

tasks.named('test') {
	useJUnitPlatform()
}

 

application.yml

spring:
  h2:
    console:
      enabled: true
  datasource:
    url: jdbc:h2:mem:jwtv1
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true
    defer-datasource-initialization: true



jwt:
  secret:

 

controller.MemberController

@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

   @PostMapping("/login")
   public ResponseEntity<String> login(@RequestBody MemberSignInResquestDto request) throws Exception {
           return ResponseEntity.ok().body(memberService.signIn(request));
   }

   @PostMapping("/join")
   @ResponseStatus(HttpStatus.OK)
    public Long join(@Valid @RequestBody MemberSignUpRequestDto request) throws Exception {
       return memberService.signUp(request);
   }


}

 

dto.MemberSignInRequestDto

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberSignInResquestDto {

    private String email;
    private String password;
}

 

dto.MemberSignUpRequestDto

@Data
@Builder
@AllArgsConstructor
public class MemberSignUpRequestDto {

    @NotBlank(message = "아이디를 입력해주세요.")
    private String email;

    @NotBlank(message = "닉네임을 입력해주세요.")
    @Size(min=2, message = "닉네임이 너무 짧습니다.")
    private String nickname;

    @NotNull(message = "나이를 입력해주세요")
    @Range(min = 0, max = 150)
    private int age;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    private String password;

    private String checkedPassword;

    private com.example.jwtv1.entity.Role role;

    @Builder
    public Member toEntity() {
        return Member.builder()
                .email(email)
                .nickname(nickname)
                .age(age)
                .password(password)
                .role(Role.USER)
                .build();
    }
}

 

entity.BaseTimeEntity

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;


}

회원가입시 유저 생성시간과 수정시간을 추가해주는 Entity입니다. 

BaseTimeEntity를 추가하면 메인Application에

@EnableJpaAuditing

위 어노테이션을 추가해야 합니다. 

@SpringBootApplication
@EnableJpaAuditing
public class JwtV1Application {

	public static void main(String[] args) {
		SpringApplication.run(JwtV1Application.class, args);
	}

}

 

entity.Member

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Entity
public class Member extends BaseTimeEntity {

    @Id @Column(name = "member_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 45, unique = true)
    private String email;

    @Column(length = 45)
    private String nickname;

    private int age;

    @Column(length = 100)
    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    public void addUserAuthority() {
        this.role = Role.USER;
    }

    public void encodePassword(PasswordEncoder passwordEncoder){
        this.password = passwordEncoder.encode(password);
    }

}

 

entity.Role

public enum Role {
    USER, MANAGER, ADMIN;
}

 

repository.MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByEmail(String email);
}

 

config.AuthenticationConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthenticationConfig {


    @Value("${jwt.secret}")
    private String secretKey;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .headers().frameOptions().sameOrigin().and()
                .authorizeRequests()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers("/api/v1/members/login").permitAll()
                .antMatchers("/api/v1/members/join").permitAll()
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(new JwtFilter(secretKey), UsernamePasswordAuthenticationFilter.class)
                .build();
    }



}

 

service.MemberService

public interface MemberService {


    public String signIn(MemberSignInResquestDto requestDto) throws Exception;


    public Long signUp(MemberSignUpRequestDto requestDto) throws Exception;
    

}

 

service.impl.MemberServiceImpl

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Value("${jwt.secret}")
    private String secretKey;

    private Long expiredMs = 1000 * 60 * 60l;


    @Override
    @Transactional
    public Long signUp(MemberSignUpRequestDto requestDto) throws Exception {
        if(memberRepository.findByEmail(requestDto.getEmail()).isPresent()){
            throw new Exception("이미 존재하는 이메일입니다.");
        }

        if(!requestDto.getPassword().equals(requestDto.getCheckedPassword())){
            throw new Exception("비밀번호가 일치하지 않습니다.");
        }


        Member member = memberRepository.save(requestDto.toEntity());
        member.encodePassword(passwordEncoder);


        member.addUserAuthority();
        return member.getId();
    }


    public String signIn(MemberSignInResquestDto requestDto){

        Member member = memberRepository.findByEmail(requestDto.getEmail())
                .orElseThrow(() -> new IllegalArgumentException("가입되지 않은 이메일 입니다."));
        if(!passwordEncoder.matches(requestDto.getPassword(), member.getPassword())){
            throw new IllegalArgumentException("잘못된 비밀번호 입니다.");
        }

        return JwtUtil.createJwt(member.getNickname(), secretKey, expiredMs);
    }

}

 

여기서 언급되지 않은 클래스들은 변경사항이 없어서 작성하지 않았습니다

이전 게시글이나 깃허브 소스코드로 확인하시면 됩니다!

https://github.com/Changha-dev/jwt-security-v1

 

GitHub - Changha-dev/jwt-security-v1

Contribute to Changha-dev/jwt-security-v1 development by creating an account on GitHub.

github.com

 

결과

회원가입
login
review

 


기존 jwt 코드에 회원가입, 로그인 로직만 추가한 거라 딱히 힘든 부분은 없었다.

 accessToken과 refreshToken으로 좀 더 완성도 있게 jwt구현 해봐야겠다