JWT토큰 Study Project

하쮸·2025년 1월 14일

JWT

목록 보기
1/1
post-thumbnail

1. 프로젝트.

  • JDK 17.
  • Spring Boot 3.4.1
  • Dependencies
    • JPA.
    • Security.
    • Validation.
    • Lombok.
    • H2
    • Web

1-1. 간단한 테스트.

@RestController
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/hello")
    public ResponseEntity<String> hello() {
        return ResponseEntity.status(HttpStatus.OK).body("hello");
    }
}
  • 위 코드를 작성한 뒤 서버를 실행 시켜서 포스트맨으로 테스트.

  • 401 Unauthorized 에러 발생.

2. 401 Unauthorized 해결.

@EnableWebSecurity
public class SecurityConfig {
}
  • @EnableWebSecurity
    • 기본적인 웹 보안을 활성화하는 에노테이션.
  • 추가적인 설정을 위해서
    WebSecurityConfigurer를 implements하거나 WebSecurityConfigurerAdapter를 extends하는 방법이 있음.
    (과거)
    • (현재)
      • Http Security설정을 하기위해서는 시큐리티 5.4버전에 도입된 SecurityFilterChain빈을 생성하여 설정해야됨.
        https://spring.io/blog...을 참고.
@EnableWebSecurity              // 기본적인 웹 보안을 활성화하는 에노테이션.
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity

                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        .requestMatchers("/api/hello").permitAll()
                        .anyRequest().authenticated()
                );
        return httpSecurity.build();
    }
}
  • /api/hello에 대한 요청은 인증 없이 접근을 허용.
  • 그외 모든 요청은 인증이 필요함.

  • 이제 문자열이 잘 응답되었음.

3. Datasource, JPA 설정.

  • application.properties 파일을 .yml로 변경.
    • 단순히 가독성을 위해서.

application.yml

spring:
  h2:
    console:
      enabled: true                           # H2 데이터베이스 콘솔을 활성화.
  datasource:
    url: jdbc:h2:mem:testdb                   # JDBC URL을 설정, 메모리 기반 H2 데이터베이스를 사용.
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop                   # SessionFactory가 시작될 때 Drop, Create, Alter를 하고, 종료될 때 Drop.
    properties:
      hibernate:
        format_sql: true                      # 콘솔창에서 SQL을
        show_sql: true                        # 보기 좋게 하기 위한 설정.
    defer-datasource-initialization: true

logging:
  level:
    me.silvernine: DEBUG                      # 로깅 레벨을 디버그로 설정.
  • ddl-auto: create-drop
    • 스프링 부트 서버가 시작될 때마다 테이블들이 새로 Create됨.
  • 편의를 위해서 resources 디렉토리data.sql을 작성해서 서버가 시작될 때마다 실행할 쿼리를 넣어줌.

4. Entity 생성.

User 엔터티

@Entity                         
@Table(name = "user")          
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    @JsonIgnore
    @Id
    @Column(name = "user_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(name = "username", length = 50, unique = true)
    private String username;

    @JsonIgnore
    @Column(name = "password", length = 100)
    private String password;

    @Column(name = "nickname")
    private String nickname;

    @JsonIgnore
    @Column(name = "activated")
    private boolean activated;

    @ManyToMany
    @JoinTable(
            name = "user_authority"
            , joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")}
            , inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;
}
  • 마지막은 권한에 대한 관계.
  • @ManyToMany, @JoinTable어노테이션은
    user(유저) 객체authority(권한) 객체다대다 관계
    일대다, 다대일 관계의 조인 테이블정의했다는 의미.
    • O >-< O 였던 관계를 O -< O >- O로 했음. (O는 테이블을 뜻함.)

Authority 엔터티

@Entity
@Table(name = "authority")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Authority {
    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName;
}
  • authorityName이라는 PK를 가짐.

5. H2 Console 결과 확인.

  • H2-Console의 요청들은 스프링 시큐리티 로직을 수행하지 않도록 설정.
@EnableWebSecurity              
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 토큰을 사용하는 방식이기 때문에 CSRF를 disable해줌.
                .csrf(AbstractHttpConfigurer::disable)


                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        .requestMatchers("/api/hello").permitAll()
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .anyRequest().authenticated()
                )
                // enable h2-console
                .headers(headers ->
                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
        return httpSecurity.build();
    }
}

  • 콘솔에 SQL 쿼리도 잘 나옴.

  • h2-console 또한 잘 들어가짐.

6. JWT 설정.

  • HS512알고리즘을 사용할 것이기 때문에 Secret Key는 512bit, 즉 64Byte 이상이 돼야함.

application.yml

jwt:
  header: Authorization
  #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야함.
  secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
  token-validity-in-seconds: 86400 		# (ms)
  • 해당 코드 추가.

build.gradle

implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
  • JWT 관련 라이브러리 추가.

7. JWT 관련 코드.

TokenProvider

@Component
public class TokenProvider implements InitializingBean {
    private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
    private static final String AUTHORITIES_KEY = "auth";
    private final String secret;
    private final long tokenValidityInMilliseconds;
    private Key key;

    public TokenProvider(@Value("${jwt.secret}") String secret
                       , @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
}
  • InitializingBean을 구현(implements)해서 afterPropertiesSet오버라이드(@Override)한 이유.
    • 빈(Bean)생성이 되고 의존성 주입까지 받은 후에 주입 받은 secret값을 Base64 Decode해서 key변수에 할당하기 위해서.
public String createToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds);       // 만료시간 설정.

        return Jwts.builder()                                   				// jwt 토큰을 생성해서 리턴.
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();
    }
  • Authentication 객체에 저장되어 있는 권한 정보들을 이용해서 토큰을 생성하는 createToken메서드 추가.
public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

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

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

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
  • 토큰을 매개변수로 받아서 토큰에 저장되어 있는 권한 정보들을 이용해서 Authentication 객체를 리턴하는 getAuthentication메서드 추가.
    public boolean validateToken(String token) {                    // 토큰을 매개변수로 받아서 토큰의 유효성 검사
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
  • 토큰을 매개변수로 받아서 토큰의 유효성 검사를 할 수 있는 validateToken메서드.
public class JwtFilter extends GenericFilterBean {

    private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
    public static final String AUTHORIZATION_HEADER = "Authorization";
    private TokenProvider tokenProvider;
    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        
    }
}
  • JWT를 위한 커스텀 필터를 만들기 위해서 JwtFilter 클래스 생성.
  • JwtFilterTokenProvider주입받음.
  • GenericFilterBean을 상속받아서 GenericFilterBeandoFilter메서드를 오버라이드.
    • 필터링 로직은 doFilter 메서드 구현부에 작성.
    • 해당 메서드의 역할은 JWT 토큰의 인증 정보시큐리티 컨텍스트(SecurityContext)저장하는 역할을 수행.
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }

        return null;
    }
  • 필터링을 하기 위해서는 토큰 정보가 있어야 하니깐 resolveToken 메서드 추가.
    • Request 헤더에서 토큰 정보를 추출하는 메서드.
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
  • httpServletRequest에서 토큰을 받아서 해당 토큰의 유효성 검증을 위한 메서드를 호출하고, 검증이 통과되면 토큰에서 Authentication 객체를 받아와서 SecurityContextHolder.getContext().setAuthentication(authentication)을 통해 넣음.

  • TokenProvider, JwtFilter를 시큐리티 설정에 적용할 클래스 추가.

JwtSecurityConfig

public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;
    public JwtSecurityConfig(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void configure(HttpSecurity http) {
        http.addFilterBefore(
                new JwtFilter(tokenProvider),
                UsernamePasswordAuthenticationFilter.class
        );
    }
}
  • SecurityConfigurerAdapter를 상속받고 TokenProvider를 주입 받아서 JwtFilter를 통해 시큐리티 로직에 필터를 등록함.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}
  • 클라이언트가 인증되지 않았거나, 유효한 인증 정보가 부족한채로 접근하려고 할 때 401 Unauthorized에러를 리턴하기 위해서 AuthenticationEntryPoint구현(implements)JwtAuthenticationEntryPoint클래스.
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
      //필요한 권한이 없이 접근하려 할때 403
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
   }
}
  • 필요한 권한이 존재하지 않는 경우에 403 Forbidden 에러를 리턴하기 위해서 AccessDeniedHandler구현(implements)JwtAccessDeniedHandler클래스.

8. JWT 관련 코드를 시큐리티 설정에 추가.

  • JWT 관련해서 만든 5개의 클래스를 시큐리티 설정에 추가.
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    public SecurityConfig(      // 생성자를 통해 주입.
            TokenProvider tokenProvider,
            CorsFilter corsFilter,
            JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
            JwtAccessDeniedHandler jwtAccessDeniedHandler
    ) {
        this.tokenProvider = tokenProvider;
        this.corsFilter = corsFilter;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 토큰을 사용하는 방식이기 때문에 csrf를 disable.
                .csrf(AbstractHttpConfigurer::disable)

                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(exceptionHandling -> exceptionHandling       // Exception 핸들링할 때 만든 예외처리 클래스를 추가.
                        .accessDeniedHandler(jwtAccessDeniedHandler)
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                )

                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        .requestMatchers("/api/hello", "/api/authenticate", "/api/signup").permitAll()      // 토큰을 받기 위한 로그인 api, 회원가입 api는 허용해줌.
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .anyRequest().authenticated()
                )

                // 세션을 사용하지 않기 때문에 STATELESS로 설정.
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                // enable h2-console
                .headers(headers ->
                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                )

                .with(new JwtSecurityConfig(tokenProvider), customizer -> {});		// JwtSecurityConfig 객체를 HttpSecurity 설정에 추가.
        return http.build();
    }
}

9. DTO.

  • 외부와의 통신을 위해 사용할 DTO클래스.
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginDto {

    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @NotNull
    @Size(min = 3, max = 100)
    private String password;
}
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenDto {         // Response할 때 사용.
    private String token;
}
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {

    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @NotNull
    @Size(min = 3, max = 100)
    private String password;

    @NotNull
    @Size(min = 3, max = 50)
    private String nickname;

    private Set<AuthorityDto> authorityDtoSet;

    public static UserDto from(User user) {
        if(user == null) return null;

        return UserDto.builder()
                .username(user.getUsername())
                .nickname(user.getNickname())
                .authorityDtoSet(user.getAuthorities().stream()
                        .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
                        .collect(Collectors.toSet()))
                .build();
    }
}

10. Repository.

public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "authorities")        // @EntityGraph : 쿼리가 수행될 때 Eager 방식으로 조회해서 authorities정보를 같이 가져옴.
    Optional<User> findOneWithAuthoritiesByUsername(String username);   // username을 기준으로 User정보를 가져올 때 권한 정보도 같이 들고옴.
}
  • @EntityGraph
    • 쿼리가 수행될 때 Eager 방식으로 조회해서 authorities정보를 같이 가져옴.
  • username을 기준으로 User정보를 가져올 때 권한 정보도 같이 들고오는 메서드.

11. UserDetailsService.

  • 시큐리티에서 제일 중요한 부분이라 할 수 있는 UserDetailsService를 커스텀해서 구현한 CustomUserDetailsService클래스 생성.
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional
    public UserDetails loadUserByUsername(final String username) {
        return userRepository.findOneWithAuthoritiesByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    private org.springframework.security.core.userdetails.User createUser(String username, User user) {
        if (!user.isActivated()) {
            throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
        }

        List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toList());

        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(),
                grantedAuthorities);
    }
}
  • UserDetailsService를 구현(implements)하고 UserRepository를 주입 받음.
  • loadUserByUsername메서드를 오버라이드 해서 로그인 시 데이터베이스에서 유저 정보를 권한 정보와 함께 가져옴.
    • 데이터베이스에서 가져온 데이터를 기준으로 해당 유저가 활성화 되어 있는 지 검증하고, 활성화 되어 있는 상태라면 유저 정보(username, password)와 권한 정보를 가지고 User 객체를 리턴.

11. 로그인 API

@RestController
@RequestMapping("/api")
public class AuthController {
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    // 생성자를 통해 주입.
    public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.tokenProvider = tokenProvider;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    @PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {

        // DTO의 정보를 이용해서 UsernamePasswordAuthenticationToken 객체를 생성.
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        // authenticationToken을 이용해서 authenticate 메서드를 호출할 때 loadUserByUsername 메서드가 실행됨.
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        // 그 결과로 Authentication 객체가 만들어지고, 이 객체를 시큐리티 컨텍스트에 저장하고
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // authentication 인증 정보를 기준으로 해서 createToken 메서드를 호출해서 JWT 토큰을 만듦.
        String jwt = tokenProvider.createToken(authentication);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
        // JWT 토큰을 응답 헤더에도 넣어주고

        // 토큰 DTO를 이용해서 응답 바디에도 넣어서 리턴.
        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
    }
}

12. Postman 테스트.

  • 토큰이 정상적으로 리턴됐음.

13. 회원가입 API.

public class SecurityUtil {

    private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    private SecurityUtil() {}

    public static Optional<String> getCurrentUsername() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            logger.debug("Security Context에 인증 정보가 없습니다.");
            return Optional.empty();
        }

        String username = null;
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
            username = springSecurityUser.getUsername();
        } else if (authentication.getPrincipal() instanceof String) {
            username = (String) authentication.getPrincipal();
        }

        return Optional.ofNullable(username);
    }
}
  • getCurrentUsername메서드의 역할은 시큐리티 컨텍스트에서 Authentication 객체를 통해서 username을 리턴해줌.
  • 시큐리티 컨텍스트에 Authentication 객체저장되는 시점은 JwtFilter의 doFilter 메서드에서 Request가 들어올 때 시큐리티 컨텍스트에서 Authentication 객체를 저장하는데 이때 저장된 Authentication 객체를 사용함.
@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    public UserDto signup(UserDto userDto) {
        if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
            throw new DuplicateMemberException("이미 가입되어 있는 유저입니다.");
        }

        Authority authority = Authority.builder()       // 권한 정보 생성.
                .authorityName("ROLE_USER")
                .build();

        User user = User.builder()
                .username(userDto.getUsername())
                .password(passwordEncoder.encode(userDto.getPassword()))
                .nickname(userDto.getNickname())
                .authorities(Collections.singleton(authority))
                .activated(true)
                .build();

        return UserDto.from(userRepository.save(user));
    }

    // username에 해당하는 유저 정보와 권한 정보를 가져옴.
    @Transactional(readOnly = true)
    public UserDto getUserWithAuthorities(String username) {
        return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null));
    }

    // 현재 시큐리티 컨텍스트에 저장되어 있는 username에 해당하는 유저 정보와 권한 정보를 가져옴.
    @Transactional(readOnly = true)
    public UserDto getMyUserWithAuthorities() {
        return UserDto.from(
                SecurityUtil.getCurrentUsername()
                        .flatMap(userRepository::findOneWithAuthoritiesByUsername)
                        .orElseThrow(() -> new NotFoundMemberException("Member not found"))
        );
    }
}
  • 하단 두 메서드의 허용 권한을 다르게 해서 권한 검증에 대한 테스트를 해볼 예정.

13. Postman 테스트.

  • 응답이 정상적으로 리턴됐음.

  • H2 console을 보면 user, hajju는 권한이 1개.
    admin은 권한을 2개 가지고 있는 것을 확인.

14. 권한 검증.

  • admin 계정으로 로그인해서 받은 토큰으로 API를 호출한 결과는 응답이 잘 나옴.

  • ROLE_USER 권한만 가지고 있는 유저로 토큰을 발급 받고

  • 해당 토큰을 가지고 API를 호출하면 403 Forbidden 에러 발생.
profile
Every cloud has a silver lining.

0개의 댓글