JWT 도입기(2) - JWT + Spring Security

김성수·2022년 12월 1일
0

JWT + Spring Security

목록 보기
2/3

참고 사이트)
https://bcp0109.tistory.com/301#recentComments

내코드)
https://github.com/jamwomsoo/JWT-Spring-Security-login-

이분꺼 보면서 테스트 프로젝트로 따라해보고 본 코드에 따로 적용함 - 내용도 거의 다 비슷

내가 JWT + Spring Security를 사용한 이유:

  • React(statless)와 함께 사용하려고 찾아보니 쿠키/세션이 아닌 토큰을 이용해서 로그인을 사용하는게 있어서 사용하게 됨

코드에 현재 Refresh token 사용하는게 있는데 다음 차수에 사용

Spring Security란?

  • 사용자 정보(ID/PW) 검증 및 유저 정보 관리 등을 쉽게 사용할 수 있도록 제공
  • 세션 기반 인증 사용

1. 의존성(build.gradle)

implementation 'org.springframework.boot:spring-boot-starter-security'

2.도메인

Member 도메인

  • Member
  • MemberRepository
  • MemberService
  • application.yml

2.1 Member Class

@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;
    }
}

Member - Authority

/**
 * Spring Security User Role을 위한 Enum 타입  Role
 */
public enum Authority {
    ROLE_USER, ROLE_ADMIN
}

2.2 MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
}
  • ID 찾기(email == ID)
  • 중복 확인(existsByEmail)

2.3 MemberService


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

    /**
     * 헤더에 있는 token 값을 토대로 Member의 data를 건내주는 메소드
     * @return
     */
    public MemberResponseDto getMyInfoBySecurity(){
        return memberRepository.findById(SecurityUtil.getCurrentMemberId())
                .map(MemberResponseDto::of)
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다."));
    }

    /**
     * 닉네임 변경 메소드
     * @param email
     * @param nickname
     * @return
     */
    @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));
    }

    /**
     * 패스워드 변경 메소드
     * token값을 토대로 찾은 Member를 통해서 예전 패스워드와 DB의 데이터와 비교
     * @param email
     * @param exPassword
     * @param newPassword
     * @return
     */
    @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));
    }
}

2.4 MemberController

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

    private final MemberService memberService;

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

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

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

2.5 application.yml


spring:

  datasource:
    url: jdbc:mariadb://localhost:3306/testdb
    driver-class-name: org.mariadb.jdbc.Driver
    username: root
    password: 1234


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

logging:
  level:
    com.tutorial: debug

jwt:
  secret: c3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXR1dG9yaWFsLWppd29vbi1zcHJpbmctYm9vdC1zZWN1cml0eS1qd3QtdHV0b3JpYWwK
  • HS512 알고리즘을 사용할 것이기 때문에, 64byte 이상의 secret key를 사용해야 함

3. JWT설정

-	TokenProvider: 유저 정보로 JWT토큰을 만들거나 토큰을 바탕으로 유저 정보를 가져옴
                 : 토큰을 생성하고 검증하는 클래스

: 해당 컴포넌트는 필터 클래스에서 사전 검증을 거칩니다.
-> AuthenticationManager에 등록되어 인증을 실질적으로 처리함

-	JWTFilter    : Spring Request 앞단에 붙일 Custom Filter

3.1 TokenProvider

@Component
@Slf4j
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;

    // annotation으로 yml에 있는 secret key를 가져온 다음 Decode함
    public TokenProvider(@Value("${jwt.secret}") String secretKey){
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);

    }

    /**
     * Describe : 토큰을 만드는 메소드
     * @param authentication
     * @return TokenDto에 생성한 Token 정보를 넣어서  return
     *
     */
    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);

        // access token 생성
        Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .setExpiration(tokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();

        //refresh token 생성
        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();
    }

    /**
     * Describe : 받은 토큰의 인증을 꺼내는 메소드
     * @param accessToken
     * @return
     *
     */
    public Authentication getAuthentication(String accessToken){
        Claims claims = parseClaims(accessToken);

        if(claims.get(AUTHORITIES_KEY) == null){
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }
        // claims 형태의 토큰을 알맞게 정렬한 이후 SimpleGrantedAuthority 형태의 새로운 list 생성 - 인가가 들어있음
        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);
    }

    /**
     * Describe : 토큰을 검증하기 위한 메소드
     * @param token
     * @return
     */
    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.debug("지원되지 않는 JWT 토큰입니다.");
        }catch (IllegalArgumentException e){
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;

    }

    /**
     * Describe : 토큰을 Claim 형태로 만드는 메소드, 이를 통해 위에서 권한 정보가 있는지 없는지 체크가 가능
     *
     * @param accessToken
     * @return
     *  String 형태의 토큰을 claims 형태로 생성
     */
    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e){
            return e.getClaims();
        }
    }
}

추가 설명

  • getAuthentication

    • JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내옴
    • userDetails 객체를 생성해 UsernamePasswordAuthenticationToken 형태로 리턴하는데 SecurityContext를 사용하기 위한 절차이다
      -> SecurityContext에 Authentication 객체를 저장하고 전달하는 역할을 수행
    • parseClaims : 만료된 토큰 정보를 꺼내기 위한 메소드
  • ValidateToken
    - Jwts 모듈이 알아서 Exception을 던진다

3.2 JwtFIlter

/**
 * Class : JwtFilter
 * <p>
 * Describe : 해당 클래스는 JwtTokenProvider가 검증을 끝낸 검증을 끝낸 jwt로부터 유저 정보를 조회해 와서 UserPasswordAuthenticationFilter로 전달합니다.
 */
equiredArgsConstructor
public class JwtFilter  extends OncePerRequestFilter {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX        = "Bearer";
    private final TokenProvider tokenProvider;

    /**
     * Request Header에서 토큰 정보를 꺼내오는 메소드
     * @param request
     * @return
     */
    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;
    }

    /**
     * 필터링을 실행하는 메소드
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     * resolveToken을 통해 토큰 정보를 꺼내온 다음, validateToken으로 토큰이 유효한지 검사를 해서 , 유효하다면 Authentication을 가져와 SecurityContext에 저장
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. Request Header에서 토큰을 꺼냄
        String jwt = resolveToken(request);

		// 2. validateTokendbgytjd rjatk
        // 3 정상 토큰일 때 Authentication을 꺼내와서 SecurityContext에 저장
        if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)){
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}
  • OncePerRequestFilter 인터페이스를 구현하기 때문에 요청 받을 때 단 한번만 실행
  • doFilterInternal
    • 실제 필터링 로직이 수행되는 곳
    • 가입/로그인/토큰재발급 이외의 모든 Request요청은 이 필터를 거쳐서 토큰의 유효성을 체크한다
    • filter를 거쳐 정상적으로 Controller가 수행되면 SecurityContext에 Member ID가 존재하는 것
    • 탈퇴로 인해 MemberID가 DB에 없는 예외 상황은 Service 단에서 고려 필요

4. Security 설정

  • JwtSecurityConfig
  • JwtAutenticationEntryPoint
  • JwtAccessDeniedHandler
  • SecurityConfig
  • SecurityUtil

4.1 JwtSecurityConfig

/**
 * SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> 인터페이스 구현체
 * 직접만든 TokenProvider와 JwtFilter를 SecurityConfig에 적용할 때 사용
 */
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;

    /**
     * TokenProvider를 주입받아서 JwtFilter를 통해 SecurityConfig안에 필터를 등록하게 되고, 스프링 시큐리티 전반적인 필터에 적용됨
     * @param http
     */
    @Override
    public void configure(HttpSecurity http){
        JwtFilter customFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

4.2 JwtAutenticationEntryPoint

유저 정보 없이 접근하면 SC_UNAUTHORIZED (401) 응답을 내림

/**
 *  유효치 않은 접근을 할 때 response에 error를 만들어주는 컴포넌트
 */
@Component
public class JwtAuthenticationEntryPoint  implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 필요한 권한이 없이 접근하려 할때 401
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

4.3 JwtAccessDeniedHandler

유저 정보는 있으나 자원에 접근할 수 있는 권한이 없는 경우 SC_FORBIDDEN (403)응답을 내려줌

/**
 *  유효치 않은 접근을 할 때 response에 error를 만들어주는 컴포넌트
 */
@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);
    }
}

4.4 SecurityConfig(내 프로젝트 내에서는 WebSecurityConfig)

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

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;


    /**
     * request로부터 받은 비밀번호를 암호화하기 위한 메소드
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
                .httpBasic().disable()                    //https만 사용
                .csrf().disable()                         // csrf 방지 막음
 // Spring Security는 기본적으로 세션을 사용             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//Rest api를 통해 세션 없이 토큰을 주고 받기 때문에 Stateless 설정

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

                .and()
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()  // 여기가 로그인 페이지-로그인 페이지만 허용
                .anyRequest().authenticated()         //  나머지 URL들은 모두 인증된 사용저들에게만 허용하게 함

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));     //JwtSecurityConfig 클래스를 통해 tokenprovider를 적용시킴

        return http.build();
    }
}

4.5 SecurityUtil

/**
 * 유저 정보가 저장되는 시점에 다루는 클래스
 * Request가 들어오면 JwtFilter의 doFilter에서 저장되는데 거기에 있는 인증정보를 꺼내서, 그 안의 ID를 반환
 */
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());
    }
}
  • JwtFilter 에서 SecurityContext 에 세팅한 유저 정보를 꺼냄
  • SecurityContext 는 ThreadLocal 에 사용자의 정보를 저장

Refresh Token (+ Redis Repository와 함께 다음 단계에서 적용)

  • RefreshToken
@Getter
@NoArgsConstructor
@Table(name = "refresh_token")
@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;
    }
}
  • RefreshTokenRepository
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByKey(String key);
}

5. 사용자 인증 과정

실제로 사용자 로그인 요청이 들어왔을 때 인증 처리 후에 JWT 토큰을 발급하는 과정을 서술함

  • Authcontroller
  • AuthService
  • CustomUserDetailService

5.1 AuthController

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;
	//회원가입
    @PostMapping("/signup")
    public ResponseEntity<MemberResponseDto> singup(@RequestBody MemberRequestDto requestDto){
        return ResponseEntity.ok(authService.singup(requestDto));
    }
	//로그인
    @PostMapping("/login")
    public ResponseEntity<TokenDto> login(@RequestBody MemberRequestDto requestDto){
        return ResponseEntity.ok(authService.login(requestDto));
    }
	// 토큰 재발급 
    @PostMapping("/reissue")
    public ResponseEntity<TokenDto> reIssue(@RequestBody TokenRequestDto tokenRequestDto){
        return ResponseEntity.ok(authService.reIssue(tokenRequestDto));
    }

}
  • MemberRequestDto [email(id임), password, nickname]으로 구성
  • TokenRequestDto  [AccessToken, RefreshToken]으로 구성

5.2 AuthService

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

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

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

    @Transactional
    public TokenDto login(MemberRequestDto requestDto){
        // 1. Login ID/PW를 기반으로 AuthenticationToken 생성
        UsernamePasswordAuthenticationToken authenticationToken = requestDto.toAuthentication();

        // 2. 실제로 검증(사용자 비밀번호 체크)이 이루어지는 부분
        // authenticate(authenticationToken) method 실행 시 CustomUserDetailService - loadUserByUsername method가 실행됨
        Authentication authentication = managerBuilder.getObject().authenticate(authenticationToken);

        // 3. 인증 정보를 기반으로 JWT 토큰 생성
       TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);

       // 4 .RefreshToken 저장
        RefreshToken refreshToken = RefreshToken.builder()
                .key(authentication.getName())
                .value(tokenDto.getRefreshToken())
                .build();

        refreshTokenRepository.save(refreshToken);

        // 5. 토큰 발급
        return tokenDto;
    }

    @Transactional
    public TokenDto reIssue(TokenRequestDto tokenRequestDto){
        // 1. Refresh Token 검증
        if(!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())){
            throw new RuntimeException("Refresh Token이 유효하지 않습니다.");
        }

        // 2. Access Token에서 Member ID 가져오기
        Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());

        // 3. 저장소에서 MemberId를 기반으로  Refresh Toeken값 가져오기
        RefreshToken refreshToken = refreshTokenRepository.findByKey(authentication.getName())
                .orElseThrow(() -> new RuntimeException("로그아웃 된 사용자입니다."));

        // 4. Refresh Token 일치하는 검사
        if(!refreshToken.getValue().equals(tokenRequestDto.getRefreshToken())){
            throw new RuntimeException("토큰의 유저 정보가 일치하지 않습니다.");
        }

        // 5. 새로운 토큰 생성
        TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);

        // 6. 저장소 정보 업데이트
        RefreshToken newRefreshToken = refreshToken.updateValue(tokenDto.getRefreshToken());
        refreshTokenRepository.save(newRefreshToken);

        // 토큰 발급
        return tokenDto;
    }
}

회원가입(SignUp)

  • 회원 존재(ID) 유무 확인 후 회원가입 진행

로그인(Login)

  • Authentication
    • 사용자가 front 화면상에서 입력한 ID/Password로 인증 정보 객체 UsernamePasswordAuthenticationToken을 생성
    • 아직 인증을 마친 객체가 아님
    • AuthenticationManager에서 authenticate메소드의 파라미터로 넘겨서 검증 후에 Authentication을 받음
  • AuthenticationManager
    • Spring Security에서 실제로 인증이 이루어지는 곳
    • Authenticate method 하나로 이루어진 인터페이스
    • 위의 코드의 Builder는 UserDetails의 유저 정보가 서로 일치하는지 검사
  • 인증이 완료된 Authentication에는 Member ID(email)이 들어있다
  • 인증 객체를 바탕으로 AccessToken + Refresh토큰을 생성
  • Refresh은 저장하고(Redis Repository), 생성된 토큰 정보를 클라이언트에 전달

재발급(Reissue)

  • Access Token + Refresh Token을 Request Body에 받아서 검증
  • Refresh Token의 만료 여부를 먼저 검사
  • Access Token을 복호화하여 유저 정보(email)를 가져오고 저장소(Redis)에 있는 Refresh Token과 Client가 전달한 Refresh Token의 일치 여부를 검사
  • 만약 일치한다면 로그인 했을 때와 동일하게 새로운 토큰을 생성해서 Client에게 전달
  • Refresh Token은 재사용하지 못하게 저장소(Redis)에서 값을 갱신

5.3 CustonUserDetailService

@Service
@RequiredArgsConstructor
public class CustomUserDetailService 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)
        );
    }
}
  • org.springframework.security.core.userdetails.UserDetailsService 인터페이스를 구현한 클래스
  • loadUserByUsername 메소드를 override함
    -> 넘겨받은 UserDetail과 Authentication의 password를 비교하여 검증하는 로직을 처리
  • DB에서 username(email)을 기바능로 값을 가져오기 때문에 아이디 존재 여부도 자동으로 검증 됨

6. API 테스트

6.1 가입

6.2 로그인

6.3 재발급

6.4 accesstoken을 헤더에 포함해서 API 호출

profile
백엔드 개발자

0개의 댓글