Spring Security + JWT + React - 01. 백엔드

june·2022년 6월 3일
11
post-thumbnail

계획 변경

Spring Security에서 원래 구현하려 했던 것은 Thymeleaf 방식이었으나, 이것은 현실적으로 서비스를 구현할 때 재사용성도 크게 없고, 실무에 쓰일 가능성도 현저히 적다고 생각하여, React로 간단한 프론트엔드를 생성한 후 통신을 주고받는 방식으로 설정하였다.

Backend 구조

DB와 그것을 연결하는 Entity는 회원을 나타내는 Member하나로 구현하기로 한다.

Member의 구성요소는 id, email, password, nickname으로 구성되어 있다.

구현기능

  • 회원가입
  • 로그인
  • Token을 통한 회원 정보 API통신
  • nickname, password 변경

상세구조

회원가입

로그인

회원정보 API 통신

이 과정에서 서버는 토큰이 유효한지 검증하는 과정을 거친다.

닉네임 변경

비밀번호 변경


Backend

백엔드 부분에서는 해당 블로그에서 많은 걸 참고했다.

dependency

build.gradle

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

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

repositories {
	mavenCentral()
}

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-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'mysql:mysql-connector-java'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'


}

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

Spring 2.7.0을 사용했으며, DB는 MYSQL을 사용했다.

주요 의존성에는 JPA, Spring Security, jjwt-api가 있다.

properties

aplication.yml

spring:

  datasource:
    url: url
    username: 
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true

logging:
  level:
    com.tutorial: debug

jwt:
  secret: 

mysql의 주소와 username, password를 입력했고,

jpa설정에서는 가독성을 위해 format_sql과 show_sql를 등록했으며

ddl-auto를 통해 db를 자동으로 생성했다.

이후 jwt에 필요한 secret key는 HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.

따라서 특정 문자열을 Base64 로 인코딩한 값을 사용했다.

Entity

/entity/Member.java

@Entity
@Getter
@Builder
@NoArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String nickname;

    @Enumerated(EnumType.STRING)
    private Authority authority;

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void setPassword(String password) { this.password = password; }
    
    @Builder
    public Member(Long id, String email, String password, String nickname, Authority authority) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.authority = authority;
    }
}

/entity/Authority.java

public enum Authority {
    ROLE_USER, ROLE_ADMIN
}

앞서 말했듯이 id, email, password, nickname로 구성되어 있으며, Spring Security에는 user의 role이 필요하므로 enum타입의 role을 추가하였다.

이후 닉네임과 비밀번호 변경에 필요한 함수인 setNicknamesetPassword를 추가했다.

Repository

/repository/MemberRepository.java

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
}

간단하게 JPARepository를 사용했다.
email로 Member를 찾는 로직과, email이 존재하는가 판별하는 로직을 추가한다.

JWT

TokenProvider

/jwt/TokenProvider.java

@Component
public class TokenProvider {

    private static final String AUTHORITIES_KEY = "auth";
    private static final String BEARER_TYPE = "bearer";
    private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
    private final Key key;



    // 주의점: 여기서 @Value는 `springframework.beans.factory.annotation.Value`소속이다! lombok의 @Value와 착각하지 말것!
    //     * @param secretKey
    public TokenProvider(@Value("${jwt.secret}") String secretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }


    // 토큰 생성
    public TokenDto generateTokenDto(Authentication authentication) {

        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();


        Date tokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);

        System.out.println(tokenExpiresIn);

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .setExpiration(tokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();

        return TokenDto.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .tokenExpiresIn(tokenExpiresIn.getTime())
                .build();
    }

    public Authentication getAuthentication(String accessToken) {
        Claims claims = parseClaims(accessToken);

        if (claims.get(AUTHORITIES_KEY) == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        UserDetails principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

하나 하나 살펴보자.

일단 제일 윗단의 AUTHORITIES_KEYBEARER_TYPE은 토큰을 생성하고 검증할 때 쓰이는 string값이다.
ACCESS_TOKEN_EXPIRE_TIME는 토큰의 만료 시간이다.
key는 JWT 를 만들 때 사용하는 암호화 키값을 사용하기 위해 security에서 불러왔다.

생성자

@Value 어노테이션으로 yml에 있는 secret key를 가져온 다음 이것을 decode한다
이후 의존성이 주입된 key의 값으로 정한다.

generateTokenDto

토큰을 만드는 메소드다.
Authentication 인터페이스를 확장한 매개변수를 받아서 그 값을 string으로 변환한다.

이후 현재시각과 만료시각을 만든 후 Jwts의 builder를 이용하여 Token을 생성한 다음

TokenDto에 생성한 token의 정보를 넣는다.

getAuthentication

토큰을 받았을 때 토큰의 인증을 꺼내는 메소드다.
아래 서술할 parseClaims 메소드로 string 형태의 토큰을 claims형태로 생성한다.

다음 auth가 없으면 exception을 반환한다.

GrantedAuthority을 상속받은 타입만이 사용 가능한 Collection을 반환한다.

그리고 stream을 통한 함수형 프로그래밍으로 claims형태의 토큰을 알맞게 정렬한 이후 SimpleGrantedAuthority형태의 새 List를 생성한다. 여기에는 인가가 들어있다.

SimpleGrantedAuthorityGrantedAuthority을 상속받았기 때문에 이 지점이 가능하다.

SimpleGrantedAuthority.java

public final class SimpleGrantedAuthority implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    ...
  }

이후 Spring Security에서 유저의 정보를 담는 인터페이스인 UserDetails에 token에서 발췌한 정보와, 아까 생성한 인가를 넣고,

이를 다시 UsernamePasswordAuthenticationToken안에 인가와 같이 넣고 반환한다.

여기서 UsernamePasswordAuthenticationToken인스턴스는 UserDetails를 생성해서 후에 SecurityContext에 사용하기 위해 만든 절차라고 이해하면 된다.

왜냐하면 SecurityContextAuthentication객체를 저장하기 때문이다.

validateToken

토큰을 검증하기 위한 메소드다.

parseClaims

토큰을 claims형태로 만드는 메소드다.

이를 통해 위에서 권한 정보가 있는지 없는지 체크가 가능하다.

JwtFilter

/jwt/JwtFilter.java

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";
    private final TokenProvider tokenProvider;


    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = resolveToken(request);

        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}
resolveToken

Request Header에서 토큰 정보를 꺼내오는 메소드다.

doFilterInternal

필터링을 실행하는 메소드다.

resolveToken을 통해 토큰 정보를 꺼내온 다음, validateToken으로 토큰이 유효한지 검사를 해서,

만약 유효하다면 Authentication을 가져와서 SecurityContext에 저장한다.

SecurityContext에서 허가된 uri 이외의 모든 Request 요청은 전부 이 필터를 거치게 되며, 토큰 정보가 없거나 유효치않으면 정상적으로 수행되지 않는다.

반대로 Request가 정상적으로 Controller까지 도착했으면 SecurityContext에 Member ID가 존재한다는 것이 보장이 된다.

JwtAuthenticationEntryPoint, JwtAccessDeniedHandler

/jwt/JwtAuthenticationEntryPoint.java

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

/jwt/JwtAccessDeniedHandler.java

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 필요한 권한이 없이 접근하려 할때 403
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

두가지 모두 유효치 않은 접근을 할때 response에 error를 만들어주는 컴포넌트다.

Config

JwtSecurityConfig

/config/JwtSecurityConfig.java

@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;

    @Override
    public void configure(HttpSecurity http) {
        JwtFilter customFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> 인터페이스를 구현하는 구현체다.

직접 만든 TokenProviderJwtFilterSecurityConfig에 적용할 때 사용한다.

메인 메소드인 configureTokenProvider를 주입받아서 JwtFilter를 통해 SecurityConfig 안에 필터를 등록하게 되고, 스프링 시큐리티 전반적인 필터에 적용된다.

WebSecurityConfig

/config/WebSecurityConfig.java

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@Component
public class WebSecurityConfig {

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                .and()
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));

        return http.build();
    }
}

하나하나 파고들면, 먼저 request로부터 받은 비밀번호를 암호화하기 위해 PasswordEncoder 빈을 생성했다.

이후 실질적인 로직인 filterChain에 있어서 참조한 블로그 에서는 WebSecurityConfigurerAdapter을 상속받아 사용했다.

그러나 Spring Security는 2022년, 해당 기능을 deprecated 했다.

대신 HttpSecurity를 Configuring해서 사용하라는 대안방식을 제시했으며, 본인 또한 그 방법을 사용했다.

https만을 사용하기위해 httpBasic을 disable했으며,

우리는 리액트에서 token을 localstorage에 저장할 것이기 때문에 csrf 방지또한 disable했다.

또한 우리는 REST API를 통해 세션 없이 토큰을 주고받으며 데이터를 주고받기 때문에 세션설정또한 STATELESS로 설정했다.

이후 예외를 핸들링하는 것에서는 이전에 작성했던 JwtAuthenticationEntryPointJwtAccessDeniedHandler를 넣었다.

모든 Requests에 있어서 /auth/**를 제외한 모든 uri의 request는 토큰이 필요하다. /auth/**는 로그인 페이지를 뜻한다.

마지막으로 전에 설정한 JwtSecurityConfig클래스를 통해 tokenProvider를 적용시킨다.

SecurityUtil

/config/SecurityUtil.java

public class SecurityUtil {

    private SecurityUtil() { }

    public static Long getCurrentMemberId() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || authentication.getName() == null) {
            throw new RuntimeException("Security Context에 인증 정보가 없습니다.");
        }

        return Long.parseLong(authentication.getName());
    }
}

SecurityContext에 유저 정보가 저장되는 시점을 다루는 클래스다.

Request가 들어오면 JwtFilter의 doFilter에서 저장되는데 거기에 있는 인증정보를 꺼내서, 그 안의 id를 반환한다.

우리는 Entity를 정할때 id의 타입을 Long으로 했기 때문에 Long을 반환한다.

DTO

/dto/TokenDto.java

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenDto {
    private String grantType;
    private String accessToken;
    private Long tokenExpiresIn;
}

토큰의 값을 헤더에서 뽑거나 헤더에서 삽입할때 쓰는 dto다.
/dto/MemberResponseDto.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberResponseDto {
    private String email;
    private String nickname;

    public static MemberResponseDto of(Member member) {
        return MemberResponseDto.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .build();
    }
}

Response를 보낼때 쓰이는 dto다.

/dto/MemberRequestDto.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberRequestDto {
    private String email;
    private String password;
    private String nickname;

    public Member toMember(PasswordEncoder passwordEncoder) {
        return Member.builder()
                .email(email)
                .password(passwordEncoder.encode(password))
                .nickname(nickname)
                .authority(Authority.ROLE_USER)
                .build();
    }

    public UsernamePasswordAuthenticationToken toAuthentication() {
        return new UsernamePasswordAuthenticationToken(email, password);
    }
}

Request를 받을 때 쓰이는 dto다. UsernamePasswordAuthenticationToken를 반환하여 아이디와 비밀번호가 일치하는지 검증하는 로직을 넣을 수 있게 된다.
/dto/ChangePasswordRequestDto.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ChangePasswordRequestDto {
    private String email;
    private String exPassword;
    private String newPassword;
}

비밀번호를 수정할 때 쓰이는 dto다. 이전의 비밀번호가 제대로 입력하지 않는다면 실행되지 않는다.

Service

이제 실제 로직들이 수행되는 장소에 대해서 서술해보자.

CustomUserDetailsService

/service/CustomUserDetailsService.java

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository.findByEmail(username)
                .map(this::createUserDetails)
                .orElseThrow(() -> new UsernameNotFoundException(username + " 을 DB에서 찾을 수 없습니다"));
    }

    private UserDetails createUserDetails(Member member) {
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(member.getAuthority().toString());

        return new User(
                String.valueOf(member.getId()),
                member.getPassword(),
                Collections.singleton(grantedAuthority)
        );
    }
}

실행한 loadUserByUsername은 받은 email을 통해 user가 실제로 존재하는지 알아보는 메소드다. 존재하지 않으면 예외를 날린다.

그렇다면 이건 어디에서 쓰일까? 나중에 서술하도록 한다.

AuthService

/service/AuthService.java

@Service
@RequiredArgsConstructor
@Transactional
public class AuthService {
    private final AuthenticationManagerBuilder managerBuilder;
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;

    public MemberResponseDto signup(MemberRequestDto requestDto) {
        if (memberRepository.existsByEmail(requestDto.getEmail())) {
            throw new RuntimeException("이미 가입되어 있는 유저입니다");
        }

        Member member = requestDto.toMember(passwordEncoder);
        return MemberResponseDto.of(memberRepository.save(member));
    }

    public TokenDto login(MemberRequestDto requestDto) {
        UsernamePasswordAuthenticationToken authenticationToken = requestDto.toAuthentication();

        Authentication authentication = managerBuilder.getObject().authenticate(authenticationToken);

        return tokenProvider.generateTokenDto(authentication);
    }

}

signup 메소드는 평범하게 회원가입을 하는 메소드로, Spring Data JPA의 주요 로직으로 구성된다.

login 메소드의 상세한 구현과정은 약간 복잡하다.

  1. login 메소드는MemberRequestDto에 있는 메소드 toAuthentication를 통해 생긴 UsernamePasswordAuthenticationToken 타입의 데이터를 가지게된다.
  2. 주입받은 Builder를 통해 AuthenticationManager를 구현한 ProviderManager를 생성한다.
  3. 이후 ProviderManager는 데이터를 AbstractUserDetailsAuthenticationProvider 의 자식 클래스인 DaoAuthenticationProvider 를 주입받아서 호출한다.
  4. DaoAuthenticationProvider 내부에 있는 authenticate에서 retrieveUser을 통해 DB에서의 User의 비밀번호가 실제 비밀번호가 맞는지 비교한다.
  5. retrieveUser에서는 DB에서의 User를 꺼내기 위해, CustomUserDetailService에 있는 loadUserByUsername을 가져와 사용한다.

여기서 위에서 만든 loadUserByUsername이 사용됨을 알 수 있다.

MemberService

/service/MemberService.java

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public MemberResponseDto getMyInfoBySecurity() {
        return memberRepository.findById(SecurityUtil.getCurrentMemberId())
                .map(MemberResponseDto::of)
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
    }

    @Transactional
    public MemberResponseDto changeMemberNickname(String email, String nickname) {
        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        member.setNickname(nickname);
        return MemberResponseDto.of(memberRepository.save(member));
    }

    @Transactional
    public MemberResponseDto changeMemberPassword(String email, String exPassword, String newPassword) {
        Member member = memberRepository.findById(SecurityUtil.getCurrentMemberId()).orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        if (!passwordEncoder.matches(exPassword, member.getPassword())) {
            throw new RuntimeException("비밀번호가 맞지 않습니다");
        }
        member.setPassword(passwordEncoder.encode((newPassword)));
        return MemberResponseDto.of(memberRepository.save(member));
}

getMyInfoBySecurity는 헤더에 있는 token값을 토대로 Member의 data를 건내주는 메소드다

changeMemberNickname는 닉네임 변경이다.

changeMemberPassword는 패스워드 변경이다. 패스워드 변경 또한 token값을 토대로 찾아낸 member를 찾아낸 다음 제시된 예전 패스워드와 DB를 비교한다.

이후 맞으면 새 패스워드로 교체한다.

Controller

서비스에서 구현한 로직들을 그대로 적용한다.

/controller/AuthController.java

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity<MemberResponseDto> signup(@RequestBody MemberRequestDto requestDto) {
        return ResponseEntity.ok(authService.signup(requestDto));
    }

    @PostMapping("/login")
    public ResponseEntity<TokenDto> login(@RequestBody MemberRequestDto requestDto) {
        return ResponseEntity.ok(authService.login(requestDto));
    }
}

/controller/MemberController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/me")
    public ResponseEntity<MemberResponseDto> getMyMemberInfo() {
        MemberResponseDto myInfoBySecurity = memberService.getMyInfoBySecurity();
        System.out.println(myInfoBySecurity.getNickname());
        return ResponseEntity.ok((myInfoBySecurity));
        // return ResponseEntity.ok(memberService.getMyInfoBySecurity());
    }

    @PostMapping("/nickname")
    public ResponseEntity<MemberResponseDto> setMemberNickname(@RequestBody MemberRequestDto request) {
        return ResponseEntity.ok(memberService.changeMemberNickname(request.getEmail(), request.getNickname()));
    }

    @PostMapping("/password")
    public ResponseEntity<MemberResponseDto> setMemberPassword(@RequestBody ChangePasswordRequestDto request) {
        return ResponseEntity.ok(memberService.changeMemberPassword(request.getExPassword(), request.getNewPassword()));
    }

}

RestControllerResponseEntity를 통해 RestFul하게 통신하는 컨트롤러를 구현했다.

구현

회원가입

로그인

// Request
POST "http://localhost:8080/auth/login"
{
    "email": "token-test@test.com",
    "password": "test1234"
}

// Response
{
    "grantType": "bearer",
    "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTY1NDI0NjMyOX0.ODqkBbI-x9RyU1i7iPG9NEKwe3zigTrLdzn49SBqlKNATrhlJLyPaR-LfdFf67JH-NprgWWNtTYesJ9eFkj0Lg",
    "tokenExpiresIn": 1654246329912
}

유저 정보 API 요청

// Request
GET "http://localhost:8080/member/me"
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTY1NDI0NjMyOX0.ODqkBbI-x9RyU1i7iPG9NEKwe3zigTrLdzn49SBqlKNATrhlJLyPaR-LfdFf67JH-NprgWWNtTYesJ9eFkj0Lg

// Response
{
    "email": "token-test@test.com",
    "nickname": "token-tester"
}

정상적으로 실행이 된다.

profile
초보 개발자

3개의 댓글

comment-user-thumbnail
2022년 9월 6일

👍👍👍👍

답글 달기
comment-user-thumbnail
2023년 1월 25일

혹시 refresh token은 짧은 지식으로는 httpOnly로 헤더를 통해 전달해야하는걸로 기억을하는데 그부분을 적용하셨나요?

1개의 답글