๐Ÿ”์ธ์ฆ/์ธ๊ฐ€ ์„œ๋ฒ„ - ์†Œ์…œ -> ์ผ๋ฐ˜์œผ๋กœ ์ „ํ–ฅํ•˜๊ธฐ(4)

์ดํ•˜์–€ยท2023๋…„ 2์›” 4์ผ
1

๐ŸŽฟ Smailegate(Winter-Devcamp)

๋ชฉ๋ก ๋ณด๊ธฐ
26/49
post-thumbnail

ํ”„๋กœ์ ํŠธ ๊ตฌํ˜„(ver.1)


๐Ÿงธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

  • ์ „์ฒด ์ฐธ๊ณ  : Spring Boot JWT Tutorial
  • ์‚ฌ์šฉ ์ •๋ณด
    DB
    MySQL : 15.1 Distrib 10.4.24-MariaDB, for Win64 (AMD64)
    Redis : 3.0.504

    Framework
    SpringBoot 2.6.8

    Program
    IntelliJ IDEA Community Edition 2022.3.1
    Xampp v3.3.0
    Postman for Windows Version 10.9.2

  • ๋ฉ”์ธ

  • ํ…Œ์ŠคํŠธ

โš™๏ธAuthService๋งŒ ์ง„ํ–‰

  • ์ด์œ ? : ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ "์ฒ˜์Œ"์ด์–ด์„œ, ๊ธฐ๋ณธ์ ์ธ ๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ ์ด์™ธ์—๋Š” ์–ด๋–ค์‹์œผ๋กœ ์งœ์•ผํ• ์ง€ ๊ฐ์ด ์•ˆ์˜ค๊ธฐ๋„ ํ•˜๊ณ , ์‹œ๊ฐ„์„ ๋„ˆ๋ฌด ๋งŽ์ด ๋“ค์ด๊ฒŒ ๋˜์–ด ๋‹ค์Œ ์„œ๋ฒ„์ธ ์•Œ๋ฆผ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ฐœํ•˜์ง€ ๋ชปํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง€๊ธฐ์— ์šฐ์„  (์ •๋ณด ์กฐํšŒ, ์ˆ˜์ •, ํƒˆํ‡ด) ์ด 3๊ฐ€์ง€๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ œ์™ธํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.
  • ๊ทธ ๋Œ€์‹ ? : ํ”„๋กœ์ ํŠธ๋ฅผ ์˜ฌ๋ฆด๋•Œ๋Š” ์ง€์šฐ๊ฒ ์ง€๋งŒ, POSTMAN์„ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธํ•  ๋•Œ ์—๋Ÿฌ๋กœ๊ทธ๊นŒ์ง€ ๋ฝ‘์•„๋‚ด์–ด ๊ฐ™์ด ํ™•์ธํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณค๋‹ค.

๐Ÿงธ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ ์„ค๋ช…

  • ํŒจํ‚ค์ง€ ๋ณ„๋กœ ์„ค๋ช… & import๋ฌธ ์ƒ๋žต
  • ํ”„๋กœ์ ํŠธ์˜ ์ŠคํŽ™์— ๋Œ€ํ•ด์„œ...
    -> ์ด๋ฒˆ ์บ ํ”„๋ฅผ ํ†ตํ•ด ์ฒ˜์Œ์œผ๋กœ ์Šคํ”„๋ง์„ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ๊ทธ๋ ‡๊ธฐ์— ์–ด๋–ค ๊ธฐ์ˆ ์„ ์ง€ํ–ฅํ•˜๊ณ  ์ง€์–‘ํ•˜๋Š” ์ˆ˜์ค€์˜ ๊ฐœ๋ฐœ์„ ์–ด๋ ค์šธ ๊ฒƒ์ด๋ผ ํŒ๋‹จํ–ˆ์œผ๋ฉฐ ๊ธฐ๋ณธ์ ์ธ ํŠœํ† ๋ฆฌ์–ผ๋กœ ๋ฐฐ์šด ๊ธฐ์ˆ ์„ ์ตœ๋Œ€ํ•œ ์‚ฌ์šฉํ•ด๋ณด๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ–ˆ๊ธฐ์— ๊ธฐ์ˆ  ์‚ฌ์šฉ์— ๋Œ€ํ•ด ๋น„ํšจ์œจ์ ์ธ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ์–‘ํ•ด๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค๐Ÿ˜ข

๐Ÿ“Œ build.gradle


๐Ÿ“Œapplication.yml


๐Ÿ—‚๏ธ config

1. initDb

  • ํ•ด๋‹น ์Šคํ”„๋ง ํ”„๋กœ์ ํŠธ๊ฐ€ ์‹คํ–‰ํ•  ๋•Œ, ํ˜ธ์ถœ๋˜์–ด DB๋ฅผ ๊ตฌ์„ฑ
  • @PostConstruct : spring web application server๊ฐ€ ์˜ฌ๋ผ์™€์„œ bean์ด ์ƒ์„ฑ, ๊ทธ๋‹ค์Œ ํ˜ธ์ถœ
package me.ver.Authserver7.config;

import...

import javax.annotation.PostConstruct;

/**
 * INSERT INTO authority (AUTHORITY_STATUS) values ('ROLE_USER');
 * INSERT INTO authority (AUTHORITY_STATUS) values ('ROLE_ADMIN');
 */
 
@Component
@RequiredArgsConstructor
public class initDb {

    private final InitService initService;

    @PostConstruct
    public void init() {
        initService.dbInit();
    }

    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {

        private final AuthorityRepository authorityRepository;

        public void dbInit() {
            authorityRepository.save(new Authority(AuthorityEnum.ROLE_ADMIN));
            authorityRepository.save(new Authority(AuthorityEnum.ROLE_USER));
        }
    }
}

2. JwtSecurityConfig

  • @RequiredArgsConstructor : ์ง์ ‘ ๋งŒ๋“  TokenProvider์™€ JwtFilter๋ฅผ SecurityConfig์— ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•จ.
  • SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { : ์ง์ ‘ ๋งŒ๋“  JwtFilter๋ฅผ Security Filter ์•ž์— ์ถ”๊ฐ€ํ•จ.
package me.ver.Authserver7.config;

import...

@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final TokenProvider tokenProvider;
    private final StringRedisTemplate redisTemplate;

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

3. SecurityConfig

  • .sessionCreationPolicy(SessionCreationPolicy.STATELESS) : ์‹œํ๋ฆฌํ‹ฐ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์ด ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด stateless๋กœ ์„ค์ •.
  • .exceptionHandling() : exception์„ ํ•ธ๋“ค๋ง ํ• ๋•Œ ์ง์ ‘ ๋งŒ๋“  ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•จ.
  • .apply(new JwtSecurityConfig(tokenProvider, redisTemplate)); : JwtFilter๋ฅผ addFilterBefore๋กœ ๋“ฑ๋กํ•œ JwtSecurityConfig์˜ ํด๋ž˜์Šค๋ฅผ ์ ์šฉํ•จ.
package me.ver.Authserver7.config;

import...

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final StringRedisTemplate redisTemplate;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

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

                .and()
                .apply(new JwtSecurityConfig(tokenProvider, redisTemplate));
    }
}

๐Ÿ—‚๏ธ controller

1. AuthController

  • ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ํ† ํฐ ์žฌ๋ฐœ๊ธ‰์„ ์ฒ˜๋ฆฌํ•˜๋Š” API
  • UserRequestDto : ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์‹œ๋„ํ•œ email๊ณผ password String์ด ์žˆ์Œ.
  • TokenRequestDto : ํ† ํฐ ์žฌ๋ฐœ๊ธ‰์„ ์œ„ํ•ด, AccessToken & RefreshToken String์ด ์žˆ์Œ.
package me.ver.Authserver7.controller;

import...

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

    // ํšŒ์›๊ฐ€์ž…
    @PostMapping("/signup")
    public ResponseEntity<UserResponseDto> signup(@RequestBody UserRequestDto userRequestDto) {
        return ResponseEntity.ok(authService.signup(userRequestDto));
    }

    // ๋กœ๊ทธ์ธ
    @PostMapping("/login")
    public ResponseEntity<TokenDto> login(@RequestBody UserRequestDto userRequestDto) {
        return ResponseEntity.ok(authService.login(userRequestDto));
    }

    // ํ† ํฐ ์žฌ๋ฐœ๊ธ‰
    @PostMapping("/reissue")
    public ResponseEntity<TokenDto> reissue(@RequestBody TokenRequestDto tokenRequestDto) {
        return ResponseEntity.ok(authService.reissue(tokenRequestDto));
    }
}

2. UserController

package me.ver.Authserver7.controller;

import...

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth/user")
@PreAuthorize("isAuthenticated()")
public class UserController {

    private final UserService userService;

    // ์ •๋ณด ์กฐํšŒ
    @GetMapping("/me")
    public ResponseEntity<UserResponseDto> getMyInfo() {
        return ResponseEntity.ok(userService.getMyInfo());
    }

    // ์ •๋ณด ์ˆ˜์ •
    @PutMapping("/update")
    public ResponseEntity<UserResponseDto> updateMyInfo(@RequestBody UserUpdateDto dto) {
        userService.updateMyInfo(dto);
        return ResponseEntity.ok(userService.getMyInfo());
    }

    // ํšŒ์› ํƒˆํ‡ด
    @DeleteMapping("/me")
    public ResponseEntity<String> deleteMember(HttpServletRequest request) {
        userService.logout(request);
        userService.deleteMember();
        return new ResponseEntity<>("ํšŒ์› ํƒˆํ‡ด ์„ฑ๊ณต", HttpStatus.OK);
    }

    // ๋กœ๊ทธ์•„์›ƒ
    @GetMapping("/logout")
    public ResponseEntity<String> logout(HttpServletRequest request) {
        userService.logout(request);
        return new ResponseEntity<>("๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต", HttpStatus.OK);
    }
}


๐Ÿ—‚๏ธ domain

1. Authority

  • @NoArgsConstructor(access = AccessLevel.PROTECTED) : ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†๋Š” ์ƒ์„ฑ์ž์˜ ์ƒ์„ฑ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ PROTECTED ์‚ฌ์šฉ(์™ธ๋ถ€์—์„œ ์ƒ์„ฑ์ž ์ƒ์„ฑ ๋ง‰๊ธฐ ์œ„ํ•จ)
  • @Builder : builder ์‚ฌ์šฉ์œผ๋กœ ์ƒ์„ฑ์ž์— ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด ์ƒ์„ฑ
package me.ver.Authserver7.domain;

import...

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Authority {

    @Id
    @Column(name = "authority_status")
    @Enumerated(EnumType.STRING)
    private AuthorityEnum authorityStatus;

    public String getAuthorityStatus() {
        return this.authorityStatus.toString();
    }

    @Builder
    public Authority(AuthorityEnum authorityStatus) {
        this.authorityStatus = authorityStatus;
    }
}

2. AuthorityEnum

  • ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ์œ„ํ•ด Enum ํƒ€์ž…์œผ๋กœ ๊ด€๋ฆฌ
package me.ver.Authserver7.domain;

public enum AuthorityEnum {
    ROLE_USER, ROLE_ADMIN
}

3. BaseTime

  • @CreatedDate : ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ์‹œ์— ๊ทธ ์‹œ๊ฐ์„ ์ž๋™์œผ๋กœ ๊ธฐ์ž…ํ•ด์คŒ
  • @LastModifiedDate : ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • ์‹œ์— ๊ทธ ์‹œ๊ฐ์„ ์ž๋™์œผ๋กœ ์ˆ˜์ •ํ•ด์คŒ
package me.ver.Authserver7.domain;

import...

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTime {

    @CreatedDate
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime created_at;

    @LastModifiedDate
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime modified_at;
}


4. RefreshToken

  • key : User ID ๊ฐ’์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋จ
  • value : refresh token String์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋จ
  • ์ €์žฅ์€ ์–ด๋””์—โ“ : ์ผ๋ฐ˜์ ์œผ๋กœ Redis๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•˜์ง€๋งŒ, Redis ์‚ฌ์šฉ ๊ฒฝํ—˜์ด ์—†์–ด ํ•ด๋‹น token์„ MySQL์— ์ €์žฅ & ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ์— Redis ์‚ฌ์šฉ(๋กœ๊ทธ์•„์›ƒ์€ RDB์˜ ๊ฒฝ์šฐ ๋ฐฐ์น˜ ์ž‘์—…์ด ํ•„์š”ํ•˜๋‚˜, ๊ตฌํ˜„์ด ์–ด๋ ค์šธ ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ•˜์—ฌ Redis ์‚ฌ์šฉํ•จ)
package me.ver.Authserver7.domain;

import...

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class RefreshToken {

    @Id
    @Column(name = "rt_key")
    private String key;

    @Column(name = "rt_value")
    private String value;

    @Builder
    public RefreshToken(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public RefreshToken updateValue(String token) {
        this.value = token;
        return this;
    }
}

5. User

  • ํšŒ์› ์ •๋ณด : Id(๊ตฌ๋ถ„์„ ์œ„ํ•จ), email, password, nickname, created_at, modified_at

  • @JoinTable : user_authority ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•˜์—ฌ ๊ฐ€์ž…ํ•œ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ ์ €์žฅ

  • ์ˆ˜์ • ๊ฐ€๋Šฅ ์ •๋ณด : password, username(์‹ค๋ช…), nickname
    -> username์˜ ๊ฒฝ์šฐ ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•  ์ˆ˜๋„ ์žˆ์Œ.

package me.ver.Authserver7.domain;

import...

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {

    @Id @GeneratedValue
    @Column(name = "user_id")
    private Long id;

    @Column(unique = true)
    private String email;
    @Column
    private String userName;
    @Column
    private String password;
    @Column
    private String nickName;

    @Column
    @CreatedDate
    private LocalDateTime createdAt;

    @Column
    @LastModifiedDate
    private LocalDateTime modifiedAt;

    @ManyToMany
    @Column
    @JoinTable(
            name = "user_authority",
            joinColumns = {@JoinColumn(name="user_id",referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_status",referencedColumnName = "authority_status")})
    private Set<Authority> authorities = new HashSet<>();

    @Builder
    public User(String email, String userName, String password, String nickName, Set<Authority> authorities, LocalDateTime createdAt, LocalDateTime modifiedAt) {
        this.email = email;
        this.userName = userName;
        this.password = password;
        this.nickName = nickName;
        this.authorities = authorities;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }

    public void updateMember(UserUpdateDto dto, PasswordEncoder passwordEncoder) {
        if(dto.getPassword() != null) this.password = passwordEncoder.encode(dto.getPassword());
        if(dto.getUserName() != null) this.userName = dto.getUserName();
        if(dto.getNickName()!= null) this.nickName = dto.getNickName();
    }
}


๐Ÿ—‚๏ธ dto

1. TokenDto

  • ๋กœ๊ทธ์ธ, ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‘๋‹ต dto
package me.ver.Authserver7.dto;

import...

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

2. TokenRequestDto

  • ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๊ด€๋ จ ์š”์ฒญ dto
package me.ver.Authserver7.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class TokenRequestDto {
    private String accessToken;
    private String refreshToken;
}

3. UserResponseDto

  • ํšŒ์›๊ฐ€์ž…์— ๋Œ€ํ•œ ์‘๋‹ต dto
package me.ver.Authserver7.dto;

import...

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UserResponseDto {

    private String email;
    private String userName;
    private String nickName;
    private Set<Authority> authorities;

    public static UserResponseDto of(User user) {
        return new UserResponseDto(
            user.getEmail(), user.getUserName(), user.getNickName(), user.getAuthorities());
    }
}

4. UserRequestDto

  • ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ์š”์ฒญ์— ์‚ฌ์šฉ๋˜๋Š” dto
package me.ver.Authserver7.dto;

import...

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequestDto {

    private String email;
    private String password;
    private String userName;

    private String nickName;

    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;
    private Set<Authority> authorities;

    public User toMember(PasswordEncoder passwordEncoder, Set<Authority> authorities) {
        return User.builder()
            .email(email)
            .password(passwordEncoder.encode(password))
            .userName(userName)
            .nickName(nickName)
            .createdAt(createdAt)
            .modifiedAt(modifiedAt)
            .authorities(authorities)
            .build();
    }

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

5. UserUpdateDto

  • ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ •๋ณด ์ˆ˜์ • ์š”์ฒญ์— ๋Œ€ํ•œ dto
package me.ver.Authserver7.dto;

import...

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserUpdateDto {

    private String userName;
    private String password;
    private String nickName;

}


๐Ÿ—‚๏ธ jwt

1. JwtAccessDeniedHandler

  • ์œ ์ €์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ์žˆ์ง€๋งŒ, ์ž์›์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ์ด ์—†๋Š” ๊ฒฝ์šฐ SC_FORBIDDEN(403) ์‘๋‹ต
package me.ver.Authserver7.jwt;

import...

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

2. JwtAuthenticationEntryPoint

  • ์œ ์ € ์ •๋ณด๊ฐ€ ์—†๋Š” ์ƒํƒœ๋กœ ์ ‘๊ทผํ•˜๋Š” ๊ฒฝ์šฐ SC_UNAUTHORIZED(401) ์‘๋‹ต
package me.ver.Authserver7.jwt;

import...

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

3. JwtFilter

  • OncePerRequestFilter ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„

  • doFilterInternal : filtering ๋กœ์ง์ด ์ˆ˜ํ–‰๋จ.

  • SecurityContext์— ์œ ์ € ์ •๋ณด ์ €์žฅ(์š”์ฒญ์ด Controller์— ๋„์ฐฉ = SecurityContext์— UserId ์กด์žฌ๊ฐ€ ๋ณด์ฆ๋จ)

    ์ˆœ์„œ
    a. String jwt = resolveToken(request); : request header์—์„œ ํ† ํฐ ๊บผ๋‚ด๊ธฐ
    b. if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { : ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ(validateToken)
    -> ์ •์ƒ ํ† ํฐ O : Authentication ๊ฐ€์ ธ์™€ SecurityContext์— ์ €์žฅ
    c. if (redisTemplate.opsForValue().get(jwt) != null) { : ๋กœ๊ทธ์•„์›ƒ ์ฒดํฌ

  • public String resolveToken(HttpServletRequest request) { : request header์—์„œ ํ† ํฐ ์ •๋ณด ๊บผ๋‚ด๊ธฐ

package me.ver.Authserver7.jwt;

import...

@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 final StringRedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        String jwt = resolveToken(request);

        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            if (redisTemplate.opsForValue().get(jwt) != null) {
                throw new RuntimeException("๋กœ๊ทธ์•„์›ƒ ๋œ ์‚ฌ์šฉ์ž ์ž…๋‹ˆ๋‹ค.");
            }
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

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

4. TokenProvider

  • ํ† ํฐ ๊ด€๋ จ ๋กœ์ง์ด ์ด๊ณณ์„ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฃจ์–ด์ง.
  • jwt.secret : application.yml์— ์ •์˜๋˜์–ด ์žˆ์Œ.
  • generateTokenDto : ๋„˜๊ฒจ๋ฐ›์€ ์œ ์ € ์ •๋ณด๋กœ accessToken & refreshToken ์ƒ์„ฑ
  • uthentication.getName() : id ๊ฐ’์„ ๊ฐ€์ ธ์˜ด.
  • ์œ ์ € & ๊ถŒํ•œ ์ •๋ณด : accessToken์—๋งŒ ๋„ฃ๋Š” ๊ฒƒ์œผ๋กœ ๊ตฌ์„ฑํ•จ.
  • refreshToken์—๋Š” ๋งŒ๋ฃŒ๋‚ ์งœ๋งŒ ๋„ฃ์Œ.
  • getAuthentication : ํ† ํฐ์„ ๋ณตํ˜ธํ™”ํ•ด accessToken์— ์žˆ๋Š” ์ •๋ณด ๊บผ๋ƒ„.
  • validateToken : ํ† ํฐ ์ •๋ณด ๊ฒ€์ฆ
  • parseClaims : ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋”๋ผ๋„, ์ •๋ณด๋ฅผ ๊บผ๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ํ•จ.
package me.ver.Authserver7.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import me.ver.Authserver7.dto.TokenDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@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;            // 30๋ถ„
    private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;  // 7์ผ
    private final Key key;

    //private static final String NICKNAME = "nickName";
    //private String nickName;


    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(","));


        //payload ๋ถ€๋ถ„ ์„ค์ •
        //Map<String, Object> payloads = new HashMap<>();
        //payloads.put("nickname", nickName);


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

        Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);

        String accessToken = Jwts.builder()
            .setSubject(authentication.getName())       // payload "sub": "name"
            //.claim(NICKNAME, User.getNickname())
            //.setClaims(payloads)
            .claim(AUTHORITIES_KEY, authorities)        // payload "auth": "ROLE_USER"
            .setExpiration(accessTokenExpiresIn)        // payload "exp": 1516239022 (์˜ˆ์‹œ)
            .signWith(key, SignatureAlgorithm.HS512)    // header "alg": "HS512"
            .compact();

        String refreshToken = Jwts.builder()
            .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
            .signWith(key, SignatureAlgorithm.HS512)
            .compact();

        return TokenDto.builder()
            .grantType(BEARER_TYPE)
            .accessToken(accessToken)
            .accessTokenExpiresIn(accessTokenExpiresIn.getTime())
            .refreshToken(refreshToken)
            .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();
        }
    }
}


๐Ÿ—‚๏ธ repository

1. AuthorityRepository

package me.ver.Authserver7.repository;

import...

import java.util.Optional;

public interface AuthorityRepository extends JpaRepository<Authority, AuthorityEnum> {
    Optional<Authority> findByAuthorityStatus(AuthorityEnum authorityStatus);
}

2. RefreshTokenRepository

package me.ver.Authserver7.repository;

import...

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
    Optional<RefreshToken> findByKey(String key);
    Optional<RefreshToken> deleteByKey(String key);
}

3. UserRepository

package me.ver.Authserver7.repository;

import...

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
}


๐Ÿ—‚๏ธ service

1. AuthService

ํšŒ์›๊ฐ€์ž… : ์ด๋ฉ”์ผ ์ค‘๋ณต ์—ฌ๋ถ€ ๊ฒ€์ฆ -> authorityRepository์—์„œ User ๊ถŒํ•œ ์ฐพ์•„ User ๊ฐ์ฒด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด ํ›„์— ์ €์žฅ

๋กœ๊ทธ์ธ

  • dto์˜ email, password๋ฅผ ๋ฐ›์•„ UsernamePasswordAuthenticationToken ๊ฐ์ฒด ์ƒ์„ฑ
  • authenticationManagerBuilder.getObject().authenticate ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋  ๋•Œ CustomUserDetailsService ์—์„œ ๋งŒ๋“ค์—ˆ๋˜ loadUserByUsername ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ณ , Authentication ๊ฐ์ฒด ์ƒ์„ฑ
  • tokenProvider.generateTokenDto()๋กœ ํ† ํฐ ์ƒ์„ฑ -> TokenDto
  • key : authentication.getName(), value : tokenDto.getRefreshToken() -> refreshTokent ๊ฐ์ฒด ์ƒ์„ฑ -> RefreshTokenRepository์— ์ €์žฅ

ํ† ํฐ ์žฌ๋ฐœ๊ธ‰

  • dto์—์„œ accessToken & refreshToken ๋ฐ›์Œ.
  • refreshToken ๊ฒ€์ฆ
  • accessToken ๋ณตํ˜ธํ™” -> Authentication ๊ฐ์ฒด ๊ฐ€์ ธ์˜ด.
  • RefreshTokenRepository : Authentication๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜จ value์™€ dto์˜ refresh Token์ด ๋˜‘๊ฐ™์€์ง€ ๊ฒ€์‚ฌ
  • โญ• -> ์ƒˆ๋กœ์šด ํ† ํฐ ์ƒ์„ฑํ•ด RefreshTokenRepository์—์„œ ์ˆ˜์ •ํ•ด์คŒ.
package me.ver.Authserver7.service;

import...

@Service
@RequiredArgsConstructor
public class AuthService {
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final AuthorityRepository authorityRepository;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    /**
     * ํšŒ์›๊ฐ€์ž…
     */
    @Transactional
    public UserResponseDto signup(UserRequestDto userRequestDto) {
        if (userRepository.existsByEmail(userRequestDto.getEmail())) {
            throw new RuntimeException("์ด๋ฏธ ๊ฐ€์ž…๋˜์–ด ์žˆ๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.");
        }

        Authority authority = authorityRepository
                .findByAuthorityStatus(AuthorityEnum.ROLE_USER).orElseThrow(()->new RuntimeException("๊ถŒํ•œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."));

        Set<Authority> set = new HashSet<>();
        set.add(authority);

        User user = userRequestDto.toMember(passwordEncoder, set);
        return UserResponseDto.of(userRepository.save(user));
    }
    /**
     * ๋กœ๊ทธ์ธ
     */
    @Transactional
    public TokenDto login(UserRequestDto userRequestDto) {
        UsernamePasswordAuthenticationToken authenticationToken = userRequestDto.toAuthentication();

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

        TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);

        RefreshToken refreshToken = RefreshToken.builder()
                .key(authentication.getName())
                .value(tokenDto.getRefreshToken())
                .build();

        refreshTokenRepository.save(refreshToken);

        return tokenDto;
    }

    /**
     * ์žฌ๋ฐœ๊ธ‰
     */
    @Transactional
    public TokenDto reissue(TokenRequestDto tokenRequestDto) {
        if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())) {
            throw new RuntimeException("Refresh Token ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        }

        Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());

        RefreshToken refreshToken = refreshTokenRepository.findByKey(authentication.getName())
                .orElseThrow(() -> new RuntimeException("๋กœ๊ทธ์•„์›ƒ ๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."));

        if (!refreshToken.getValue().equals(tokenRequestDto.getRefreshToken())) {
            throw new RuntimeException("ํ† ํฐ์˜ ์œ ์ € ์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        }

        TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);

        RefreshToken newRefreshToken = refreshToken.updateValue(tokenDto.getRefreshToken());
        refreshTokenRepository.save(newRefreshToken);

        return tokenDto;
    }
}

2. CustomUserDetailsService

  • SpringSecurity์˜ UserDetailsService ๊ตฌํ˜„ ํด๋ž˜์Šค
  • ๋กœ๊ทธ์ธ ์‹œ DB์—์„œ ์œ ์ € ์ •๋ณด ๊ฐ€์ ธ์˜ด(UserReprsitory์—์„œ)
  • ๊ฐ€์ ธ์˜จ ์œ ์ € ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ userdetails ๊ฐ์ฒด๋กœ ๋งŒ๋“ค์–ด ๋ฆฌํ„ด
package me.ver.Authserver7.service;

import...

public class CustomUserDetails implements UserDetails {

    private String email;
    private String password;
    private String AUTHORITY;
    private boolean ENABLED;
    private String nickName;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
        auth.add(new SimpleGrantedAuthority(AUTHORITY));
        return auth;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return email;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return ENABLED;
    }

    public String getNickname() {
        return nickName;
    }

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

}


  1. UserService
์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”

๐Ÿ—‚๏ธ service > exception(์ˆ˜์ • ํ•„์š”)

  • (์ข…์ค€๋‹˜ ์ฝ”๋“œ ์‚ฌ์šฉ ๋ฐ ์ˆ˜์ •)

a. AuthServiceException

package me.ver.Authserver7.service.exception;

public enum AuthServiceException {

    EXIST_AUTH("1101", "exist AUTH"),

    NO_SUCH_AUTH("1102", "no such AUTH"),
    ALREADY_GENERATE_PASSWORD("1103", "already generate password"),
    NO_PASSWORD("1104", "no generated password"),
    NO_MATCH_PASSWORD("1105", "no match password"),
    ;

    private String message;
    private String code;

    AuthServiceException(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public String getCode() {
        return code;
    }
}


b. AuthServiceValidateException

package me.ver.Authserver7.service.exception;

import...

@Getter
public class AuthServiceValidateException extends RuntimeException {

    private final String code;

    public AuthServiceValidateException(UserServiceException publisherServiceException) {
        super(publisherServiceException.getMessage());
        this.code = publisherServiceException.getCode();
    }

}

c. UserServiceException

package me.ver.Authserver7.service.exception;

public enum UserServiceException {

    EXIST_USER("1101", "exist user"),

    NO_SUCH_USER("1102", "no such user"),
    ALREADY_GENERATE_PASSWORD("1103", "already generate password"),
    NO_PASSWORD("1104", "no generated password"),
    NO_MATCH_PASSWORD("1105", "no match password"),
    ;

    private String message;
    private String code;

    UserServiceException(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public String getCode() {
        return code;
    }
}


d. UserServiceValidateException

package me.ver.Authserver7.service.exception;

import...

@Getter
public class UserServiceValidateException extends RuntimeException {

    private final String code;

    public UserServiceValidateException(UserServiceException publisherServiceException) {
        super(publisherServiceException.getMessage());
        this.code = publisherServiceException.getCode();
    }

}


๐Ÿ—‚๏ธ util

  1. SecurityUtil
  • JwtFilter์—์„œSecurityContext์— ์„ธํŒ…ํ•œ ์œ ์ € ์ •๋ณด ๊บผ๋ƒ„
  • id๋ฅผ ์ €์žฅ -> Long ํƒ€์ž…์œผ๋กœ ํŒŒ์‹ฑ
  • SecurityContext : ThreadLocal์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
package me.ver.Authserver7.util;

import...

public class SecurityUtil {

    private SecurityUtil() { }


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

        if (authentication == null || authentication.getName() == null) {
            throw new RuntimeException("๋กœ๊ทธ์ธ ์œ ์ € ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
        }

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


๐Ÿ—‚๏ธ web(์ˆ˜์ • ์ „ ver)

  • ์„ฑ๊ณต ์ฝ”๋“œ์™€ ์‹คํŒจ ์ฝ”๋“œ๋ฅผ ์ปค์Šคํ…€ํ•˜๋Š” ๊ฒƒ์ด ์•„์ง์€ ๋ฏธ์ˆ™ํ•ด์„œ ๋งŽ์ด ์•Œ์•„๋ณด์•˜์œผ๋‚˜ ์ ์šฉ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ๊นŠ์–ด์ ธ ์šฐ์„ , ์ข…์ค€๋‹˜์˜ web ๋ถ€๋ถ„์„ ๊ฐ€์ ธ์™€ ๋‚ด ๋ถ€๋ถ„์— ๋งž๊ฒŒ ์‚ด์ง๋งŒ ๋ฐ”๊ฟ”๋‘” ์ƒํƒœ.

1. web > exception

a. ExcontrollerAdvice

package me.ver.Authserver7.web.exception;

import...

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {

    private static void logger(Exception exception) {
        log.error(exception.getClass()
                .getSimpleName() + " = [{}][{}]",
            exception.getClass(), exception.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({UserServiceValidateException.class})
    public ApiResponse<ApiResponse.FailureBody> PublisherExHandler(UserServiceValidateException exception) {
        logger(exception);
        return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + exception.getCode(),
            exception.getClass()
                .getSimpleName(), exception.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({AuthServiceValidateException.class})
    public ApiResponse<ApiResponse.FailureBody> RoomExHandler(AuthServiceValidateException exception) {
        logger(exception);
        return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + exception.getCode(),
            exception.getClass()
                .getSimpleName(), exception.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({IllegalStateException.class})
    public ApiResponse<ApiResponse.FailureBody> illegalStateExHandler(IllegalStateException exception) {
        logger(exception);
        String defaultMessage = exception.getMessage();
        return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "001",
            exception.getClass()
                .getSimpleName(), defaultMessage);
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public List<ApiResponse<ApiResponse.FailureBody>> bindingExHandler(BindException exception) {
        logger(exception);
        List<ApiResponse<ApiResponse.FailureBody>> errorResults = new ArrayList<>();
        exception.getAllErrors()
            .forEach(error -> {
                FieldError fieldError = (FieldError) error;
                String field = fieldError.getField();
                String defaultMessage = fieldError.getDefaultMessage();
                ApiResponse<ApiResponse.FailureBody> bindingException = ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "1001", exception.getClass()
                    .getSimpleName(), field + " ํ•„๋“œ ์ž…๋ ฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. " + defaultMessage);
                errorResults.add(bindingException);
            });
        return errorResults;
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler
    public ApiResponse<ApiResponse.FailureBody> exHandler(Exception exception) {
        logger(exception);
        String defaultMessage = exception.getMessage();
        return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "1000",
            exception.getClass()
                .getSimpleName(), defaultMessage);
    }
}

2. response

a. ApiResponse

package me.ver.Authserver7.web.response;

import...

@Getter
public class ApiResponse<T> extends ResponseEntity<T> {

    public ApiResponse(T body, HttpStatus status) {
        super(body, status);
    }


    @Getter
    public static class FailureBody implements Serializable {
        private Timestamp timestamp;
        private String code;
        private String error;
        private String message;

        public FailureBody(final String code, final String error, final String message) {
            this.timestamp = new Timestamp(System.currentTimeMillis());
            this.code = code;
            this.error = error;
            this.message = message;
        }
    }

    @Getter
    public static class withData<T> implements Serializable {
        private Timestamp timestamp;
        private String code;
        private String message;
        private T data;

        public withData(T data, String code, String message) {
            this.timestamp = new Timestamp(System.currentTimeMillis());
            this.code = code;
            this.message = message;
            this.data = data;
        }
    }

    @Getter
    public static class withCodeAndMessage implements Serializable {
        private Timestamp timestamp;
        private String code;
        private String message;

        public withCodeAndMessage(String code, String message) {
            this.timestamp = new Timestamp(System.currentTimeMillis());
            this.code = code;
            this.message = message;
        }
    }
}

b. ApiResponseGenerator

package me.ver.Authserver7.web.response;

import...

@UtilityClass
public class ApiResponseGenerator {

    public static <D> ApiResponse<ApiResponse.withData> success(final D data,
        final HttpStatus status,
        final String code, final String message) {
        return new ApiResponse<>(new ApiResponse.withData<>(data, code, message), status);
    }

    public static ApiResponse<ApiResponse.withCodeAndMessage> success(
        final HttpStatus status,
        final String code, final String message) {
        return new ApiResponse<>(new ApiResponse.withCodeAndMessage(code, message), status);
    }

    public static ApiResponse<ApiResponse.FailureBody> fail(
        final HttpStatus status,
        final String code, final String error, final String message) {
        return new ApiResponse<>(new ApiResponse.FailureBody(code, error, message), status);
    }
}


๐Ÿงธ ํ˜„์žฌ๊นŒ์ง€์˜ ๊ฒฐ๊ณผ

0. ํ”„๋กœ์ ํŠธ ์‹คํ–‰

  • DB

  • ์—ฌ๋Ÿฌ ์ฒ˜๋ฆฌ ํ›„

1. ํšŒ์›๊ฐ€์ž…

  • ์„ฑ๊ณต

  • ์‹คํŒจ

2. ๋กœ๊ทธ์ธ

  • ์„ฑ๊ณต

  • ์‹คํŒจ

3. ๋กœ๊ทธ์•„์›ƒ

  • ์„ฑ๊ณต

  • ์‹คํŒจ

4. ์ •๋ณด ์กฐํšŒ

  • ์„ฑ๊ณต

  • ์‹คํŒจ

5. ์ •๋ณด ์ˆ˜์ •

  • ์„ฑ๊ณต

  • ์‹คํŒจ

6. ํšŒ์› ํƒˆํ‡ด

  • ์„ฑ๊ณต

  • ์‹คํŒจ

7. ํ† ํฐ ์žฌ๋ฐœ๊ธ‰

  • ์„ฑ๊ณต

  • ์‹คํŒจ

๐Ÿงธ ํ•ด๊ฒฐ์ด ์–ด๋ ค์šด ๊ฒƒ๋“ค

  1. ํ† ํฐ์— ๋‹‰๋„ค์ž„ ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ (payload ์•ˆ)
  1. ์‹คํŒจ ์‘๋‹ต์€ ์ •ํ•ด๋‘” ๊ทœ์น™์— ๋งž๊ฒŒ ์ถœ๋ ฅ์ด ๋˜๋Š”๋ฐ, ์„ฑ๊ณต ์‘๋‹ต์ด ์›ํ•˜๋Š”๋Œ€๋กœ ์ถœ๋ ฅ์ด ์•ˆ๋˜๋Š” ์ƒํƒœ
  • ๋ฐ”๋žŒ์งํ•œ ์„ฑ๊ณต ์‘๋‹ต
{
	"result": "success"
	"data": null // ๋ณด๋‚ด์ค„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๋ฉด null๋กœ ๋ณด๋‚ด๋Š” ๊ฒƒ.
}
{
	"result": "success"
	"data" {
  				...
	}
}
profile
์–ธ์  ๊ฐ€ ๋‚ด ์ฝ”๋“œ๋กœ ์„ธ์ƒ์— ๊ธฐ์—ฌํ•  ์ˆ˜ ์žˆ๋„๋ก, BE&Data Science ๊ฐœ๋ฐœ ๊ธฐ๋ก ๋…ธํŠธโ˜˜๏ธ

0๊ฐœ์˜ ๋Œ“๊ธ€