[SpringBoot] Spring Security JWT 인증과 인가 - (1) 회원 가입

2023. 8. 14. 03:01
반응형

 

 

이번 포스팅에서는 Spring Boot에서 JWT를 이용한 인증과 인가를 다루기 이전, 사용자를 회원가입시키는 API 로직을 작성하고, 비밀번호를 해시처리하는 작업을 우선적으로 진행해보도록 하겠다.

 

JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다.

 

https://sjh9708.tistory.com/46

 

[Web] 인증과 인가 - JWT 토큰 인증

앞 포스팅에서 세션 방식의 인증과, 성능 개선을 위한 방법들에 대해서 다루어 보았었는데 이번에는 언급했던 토큰 인증 방식에 대해서 알아보려고 한다. 토큰 인증 세션 인증 방식과 달리 인증

sjh9708.tistory.com

 

 


Spring Security란?

 

스프링 프레임워크 기반의 애플리케이션에서 보안과 인증을 처리하기 위한 모듈.

Spring Security는 애플리케이션의 보안을 간단하고 유연하게 구현할 수 있도록 다양한 기능을 제공한다.

 

  • 인증(Authentication): 사용자가 신원을 증명하고 로그인하는 과정. Spring Security는 사용자의 로그인 정보를 기반으로 인증을 처리한다.
  • 인가(Authorization): 인증된 사용자가 특정 리소스 또는 기능에 접근할 권한이 있는지를 결정하는 과정. Spring Security는 사용자의 권한을 관리하고 보호된 리소스에 대한 접근을 허용하거나 거부하는데 사용된다.
  • 보안 필터 체인(Security Filter Chain): Spring Security는 여러 개의 보안 필터로 구성된 필터 체인을 제공. 각 필터는 특정한 보안 작업을 처리하며, 이 필터 체인은 요청의 인증 및 인가 과정을 처리하고 보안 관련 기능을 구현한다.
  • 세션 관리(Session Management): Spring Security는 세션 관리를 통해 사용자의 인증 상태를 유지하고 관리한다. 세션 공격을 방지하거나 세션 유지 정책을 구성할 수 있다.
  • CSRF(Cross-Site Request Forgery) 보호: Spring Security는 CSRF 공격을 방지하기 위한 기능을 제공한다.
  • 다양한 인증 인가 방식 지원: Spring Security 다양한 인증 인가 방식을 지원한다. 예를 들어, 인증, 기본 인증, OAuth, OpenID Connect, JWT

 

 

 


의존성 모듈 추가

 

build.gradle

dependencies {
	//...
    
	//security
	implementation 'org.springframework.boot:spring-boot-starter-security'

	// jwt
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

Spring Security 모듈과 JWT 사용을 위한 라이브러리들을 의존성에 추가한다.

 

 

 

 

 


SecurityConfig 작성

 

SecurityConfig.java

/common/auth/SecurityConfig.java

/**
 * [Spring Security Config 클래스]
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .anyRequest().permitAll();

        http
                .headers()
                .frameOptions()
                .sameOrigin();

        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .cors()
                .and()
                .formLogin().disable()             
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

해당 클래스는 Spring Security를 사용하여 애플리케이션의 보안 설정을 구성한다.

  • @Configuration: 이 클래스가 Spring의 구성 클래스임을 나타내며. Spring Security 설정을 포함한다.
  • @EnableWebSecurity: Spring Security를 활성화시킨다.
  • WebSecurityConfigurerAdapter: Spring Security에서 제공하는 구성 클래스 중 하나로, 확장하여 사용자 정의 보안 설정을 할 수 있다.
  • configure(HttpSecurity http): HTTP 보안 설정을 구성한다. 이 메서드를 오버라이드하여 원하는 보안 정책을 설정할 수 있다.
    • authorizeRequests(): 요청에 대한 접근 권한 설정을 시작합니다.
    • headers().frameOptions().sameOrigin(): Spring Security의 X-Frame-Options를 설정하여 같은 출처에서의 iframe으로의 접근만 허용한다. 보통 H2 데이터베이스 콘솔과 같이 iframe으로 렌더링되는 경우에 사용된다고 한다.
    • httpBasic().disable(), csrf().disable(): HTTP 기본 인증과 CSRF 보안을 비활성화한다.
    • sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS): 세션 기반의 인증을 사용하지 않도록 한다.
    • cors(): Cross-Origin Resource Sharing (CORS) 설정을 활성화한다.
    • formLogin().disable(): 로그인을 비활성화한다.

  • @Bean: BCryptPasswordEncoder 비밀번호를 암호화하는 사용되는 해시 방식이다. 해당 암호화 방식을 Bean으로 저장하여 다른 클래스에서 사용 가능하도록 한다.

 

현재 작성한 SecurityConfig는 JWT 토큰 기반 인증과 인가가 구현되지 않은 상태이다.

우선 로그인 이전에 회원 가입 로직을 작성한 이후에 JWT 인증과 인가를 이어서 구현해보도록 하자.

 

 

 


회원가입 API 작성

 

 MemberApiController


@Api(tags = "회원 API")
@RestController
@RequiredArgsConstructor
public class MemberApiController {
    private final MemberService memberService;

    private final ModelMapper modelMapper;

    @ApiOperation(
            value = "회원 신규 추가(가입)"
    )
    @PostMapping("/api/v1/member")
    public ResponseEntity<Long> addMember(@Valid @RequestBody MemberRequestDto member){
        Member entity = modelMapper.map(member, Member.class);
        Long id = memberService.join(entity);
        return ResponseEntity.status(HttpStatus.OK).body(id);
    }

}

Request로 필요한 회원정보들을 받아서 가입시키는 API 컨트롤러를 작성하였다.

 

 

 

 

 MemberRequestDto.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "회원 추가 요청 DTO")
public class MemberRequestDto {
    @ApiModelProperty(value = "회원 닉네임", required = true, example = "Tom")
    @NotBlank(message = "닉네임은 필수 입력 값입니다.")
    private String nickname;

    @ApiModelProperty(value = "회원 이메일", required = true, example = "Tom123@aaa.com")
    @NotBlank(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식에 맞지 않습니다.")
    private String email;

    @ApiModelProperty(value = "비밀번호", required = true, example = "Qqwert123#")
    @NotBlank(message = "비밀번호는 필수 입력 값입니다.")
    @Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
            message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.")
    private String password;

    @ApiModelProperty(value = "회원 계정 타입", required = true, example = "CONSUMER")
    @NotBlank(message = "계정 타입은 필수 입력 값입니다.")
    @Pattern(regexp = "^(CONSUMER|SELLER)$", message = "계정 타입은 CONSUMER, SELLER 중 하나여야 합니다.")
    private String accountType;

    //...

}

주요 회원 정보에는 닉네임, 이메일, 비밀번호, 계정유형이 들어갈 것이다.

이메일을 ID로 사용할 것이고, 계정유형 CONSUMER, SELLER에 따라서 사용할 수 있는 API 권한을 차등부여할 것이다.

 

 

 


 MemberService.java

public interface MemberService {

    /**
     * [회원 가입]
     * 회원 정보를 입력받아, 회원가입시킨다.
     * @param [member 회원 엔티티 모델(id=null)]
     * @return [성공 시 회원 PK 반환]
     */
    public abstract Long join(Member member);
    
    //...
}

 

MemberServiceImpl.java

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;
    private final BCryptPasswordEncoder encoder;

    @Transactional
    public Long join(Member member){
        Optional<Member> vailMember = memberRepository.findByEmail(member.getEmail());
        if(vailMember.isPresent()) {
            throw new ValidateMemberException("This member email is already exist. " + member.getEmail());
        }
        // 비밀번호 해시 처리
        member.encodePassword(encoder.encode(member.getPassword()));
        memberRepository.create(member);
        return member.getMemberId();
    }

	//...
}

Service 단에서 회원가입시키는 비즈니스 로직을 구현한다.

 

1. 중복된 이메일이 있다면 예외를 던진다.

2. 요청으로 들어온 비밀번호를 해시 처리한다. 이 때 사용되는 BCryptPasswordEncoder은 위에서 SecurityConfig에서 @Bean으로 등록되었으며, 의존성 주입받아 사용하게 된다.

3. 암호화된 비밀번호를 포함하여 회원 정보를 데이터베이스에 저장한다.

 

 

 MemberRepository.java

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em;

    /**
     * [Member 엔티티 추가]
     * 회원 추가
     * @param [Member Member 엔티티(pk=null)]
     */
    public void create(Member member){
        em.persist(member);
    }
    
    /**
     * [Member 엔티티 단일조회 by Email]
     * 로그인 시, 이메일로 회원 조회.
     * @param [email 이메일]
     * @return [Member || Null]
     */
    public Optional<Member> findByEmail(String email){
        TypedQuery<Member> typedQuery = em.createQuery("select m from Member m where m.email = :email", Member.class);
        typedQuery.setParameter("email", email);
        try{
            Member member = (Member) typedQuery.getSingleResult();
            return Optional.ofNullable(member);
        } catch (NoResultException e){
               return Optional.empty();
        }
    }

}

위의 서비스들에서 사용되는 Repository 메서드들이다. 설명을 보면 알겠지만, 엔티티를 추가하는 메서드와, 이메일을 기반으로 회원을 조회하는 기능을 한다.

 

 

 

 

 


결과 확인

 

 

 

Swagger 상에서 잘 동작하는지 확인하고, 데이터베이스에 등록된 것을 확인한다. 패스워드가 암호화되어 데이터베이스상에 저장된 것을 확인할 수 있다.

 

 

 

이제 다음 포스팅에서는 이렇게 회원가입된 유저들이 올바른 인증 정보로 로그인 시, JWT 토큰을 발급해주고,

다른 API 요청 시 해당 토큰을 검증하여 권한이 있는 유저들에게만 사용을 허용해주도록 인가 처리를 해보도록 하겠다.

 

 

 

 

 

https://sjh9708.tistory.com/84

 

[SpringBoot] Spring Security JWT 인증과 인가 - (2) 로그인 인증 (Authentication)

이번 포스팅에서는 Spring Boot에서 JWT를 이용한 로그인 인증을 구현해보도록 하겠다. JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다. https://sjh9708.tistory.com/46 [Web] 인증과

sjh9708.tistory.com

 

https://sjh9708.tistory.com/85

 

[SpringBoot] Spring Security JWT 인증과 인가 - (3) API 인가 (Authorization)

이전 포스팅에서 Spring Boot에서 JWT를 이용한 로그인 인증을 구현하였다. 이번에는 로그인한 사용자에 한해서 API 사용을 승인하는 API 인가(Authorization), 더불어 권한별로 인가를 구현하는 방법을

sjh9708.tistory.com

 

반응형

BELATED ARTICLES

more