Spring Security + JWT로 인증 인가 구현하기

ssongkim·2022년 4월 30일
13

Overview

JWT(Json Web Token)이란?

JWT는 RFC7519 웹 표준으로 JSON 객체를 이용해 데이터를 주고받을 수 있도록한 웹 토큰입니다.

JWTheader, payload, signature로 구성되어 있으며 headersignature를 해싱하기 위한 알고리즘 정보가 담겨있고 payload는 실제로 사용될 데이터들이 담겨 있습니다.
signature는 토큰의 유효성 검증을 위한 문자열로 이 문자열을 통해 이 토큰이 유효한 토큰인지 검증 가능합니다.

JWT는 인터페이스이고 그 구현체인 JWS, JWE가 존재합니다. 해당 게시글에서는 JWS를 응용하며 JWT에 대한 자세한 게시글은 다음에 작성하도록 하겠습니다.

1. 스프링 프로젝트 생성

로컬 개발 환경 구성

먼저 자바21로 개발환경을 구성해주셔야 합니다!
윈도우/맥 터미널에 java --version 을 입력했을 때 자바 21이 출력되도록 환경구성을 해주세요.

또한 인텔리제이는 최소 23.3 버전으로 업데이트 해주셔야 합니다.

프로젝트 생성

그다음 spring initializer에서 스프링 프로젝트를 생성합니다.

의존성은 별도로 추가할 예정이오니 아무것도 추가 안하셔도 됩니다.

2. 의존성 설정

build.gradlespring WEB, JPA, H2, lombok,security, oauth2-resource-server, security-test, spring validation 의존성을 기입합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    annotationProcessor 'org.projectlombok:lombok'

    implementation 'org.flywaydb:flyway-core:9.16.0'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
}

oauth2-resource-server는 클라이언트 및 인가서버와의 통신을 담당하는 리소스 서버의 기능을 필터 기반으로 구현한 모듈로 클라이언트 리소스 접근 제한, 토큰 검증을 위한 인가서버와의 통신 등의 구현이 가능합니다.
JWT 토큰을 생성하고 이를 검증하는데 있어 많은 추상화를 해주므로, 이를 간편하게 사용하기 위해 해당 의존성을 스프링 시큐리티에 이어 추가로 주입해줍니다.

3. application.yml 작성

spring:

  h2:
    console:
      enabled: true
      path: /h2

  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test;mode=mysql
    username: sa
    password:

  flyway:
    enabled: true
    baseline-on-migrate: true
    locations: classpath:db/migration/{vendor},classpath:db/seed/local # when you want to give test seed, add location test seed too

  jpa:
    show-sql: true
    generate-ddl: false
    hibernate:
      ddl-auto: validate

    properties:
      hibernate:
        format_sql: true

jwt:
  access-private-key: classpath:secret/accessKey
  access-public-key: classpath:secret/accessKey.pub
  access-token-validity-in-seconds: 600

access-token-validity-in-seconds은 우리가 발급할 액세스토큰의 유효기간을 지정합니다.
jwt.access-private-key와 jwt.access-public-key에는 아래에서 설명할 JWT 암복호화에 사용할 키의 경로를 지정합니다.

4. 비대칭키 생성

맥이나 리눅스 계열 OS를 사용하신다면 openssl 명령어를 이용해 비대칭키를 생성할 수 있습니다.

개인키 생성

openssl genrsa -out accessKey 2048

비대칭키를 이용해 공개키 생성

openssl rsa in accessKey -out accessKey.pub -pubout

생성 후 resources/secret 하위 디렉토리에 위치시킵니다.

entity 패키지 생성 및 작성

Authority

@Entity
@Table(name = "authority")
@Getter
@NoArgsConstructor
public class Authority {

    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName;

    @Builder
    public Authority(String authorityName) {
        this.authorityName = authorityName;
    }
}

Account

@Entity
@Table(name = "account")
@Getter
@NoArgsConstructor
public class Account {

    @Id
    @Column(name = "account_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

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

    @Column(name = "nickname", length = 50)
    private String nickname;

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

    @ManyToMany
    @JoinTable( // JoinTable은 테이블과 테이블 사이에 별도의 조인 테이블을 만들어 양 테이블간의 연관관계를 설정 하는 방법
            name = "account_authority",
            joinColumns = {@JoinColumn(name = "account_id", referencedColumnName = "account_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;

    @Builder
    public Account(String username, String password, String nickname, Set<Authority> authorities, boolean activated) {
        this.username = username;
        this.password = password;
        this.nickname = nickname;
        this.authorities = authorities;
        this.activated = activated;
        this.tokenWeight = 1L; // 초기 가중치는 1
    }

}

Authority는 인가에 사용되는 권한들을 DB로 관리하고자 생성한 엔티티입니다.
Account는 인증에 사용되는 계정 엔티티입니다.

data.sql

현재 embedded H2를 사용하며 JPAcreate-drop 설정에 따라 스프링 부트 애플리케이션이 실행될 때마다 데이터베이스에 있는 데이터들은 전부 날라갈 것입니다.

해당 게시글은 예제이므로 초기 데이터를 넣어주기 위해 resources/data.sql을 작성합니다. 실제로 개발환경에서 초기 권한데이터를 넣어줄 땐 flyway를 사용해보아요.

INSERT INTO ACCOUNT (ACCOUNT_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (1, 'admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1);
INSERT INTO ACCOUNT (ACCOUNT_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (2, 'user', '$2a$08$UkVvwpULis18S19S5pZFn.YHPZt3oaqHZnDwqbCW9pft6uFtkXKDC', 'user', 1);

INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_MEMBER');
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_ADMIN');

INSERT INTO ACCOUNT_AUTHORITY (ACCOUNT_ID, AUTHORITY_NAME) values (1, 'ROLE_MEMBER');
INSERT INTO ACCOUNT_AUTHORITY (ACCOUNT_ID, AUTHORITY_NAME) values (1, 'ROLE_ADMIN');
INSERT INTO ACCOUNT_AUTHORITY (ACCOUNT_ID, AUTHORITY_NAME) values (2, 'ROLE_MEMBER');

JWT 설정

JwtProperties

@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

    private RSAPublicKey accessPublicKey;
    private RSAPrivateKey accessPrivateKey;
    private Long accessTokenValidityInSeconds;
}

application.yml에 기입한 정보를 객체로 매핑하여 사용하기 위해 선언합니다.

JwtConfig(JWT 의존성 주입 설정파일)

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(JwtProperties.class)
public class JwtConfig {

    @Bean(name = "accessJwtDecoder")
    public JwtDecoder accessJwtDecoder(JwtProperties jwtProperties) {
        return NimbusJwtDecoder.withPublicKey(jwtProperties.getAccessPublicKey()).build();
    }

    @Bean(name = "accessJwtEncoder")
    public JwtEncoder accessJwtEncoder(JwtProperties jwtProperties) {
        JWK jwk = new RSAKey.Builder(jwtProperties.getAccessPublicKey())
                .privateKey(jwtProperties.getAccessPrivateKey())
                .build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }

    
    @Bean(name = "accessTokenProvider")
    public AccessTokenProvider accessTokenProvider(JwtEncoder accessJwtEncoder,
                                                   JwtProperties jwtProperties) {
        return new AccessTokenProvider(accessJwtEncoder, jwtProperties.getAccessTokenValidityInSeconds());
    }
}

JwtConfig는 JWT 설정파일로 AccessTokenProvider에 의존성을 주입하고 빈을 생성하는 역할을 수행합니다.
JWT 토큰 인코딩, 디코딩의 책임을 가지는 JwtDecoder 빈, JwtEnCoder 빈도 이쪽 설정파일에서 생성합니다.

AccessTokenProvider

public final class AccessTokenProvider {

    private final JwtEncoder jwtEncoder;
    private final long tokenValidityInSeconds;
    private static final String AUTHORITIES_KEY = "scp"; // spring security 기본값 (scope)

    public AccessTokenProvider(JwtEncoder jwtEncoder, long tokenValidityInSeconds) {
        this.jwtEncoder = jwtEncoder;
        this.tokenValidityInSeconds = tokenValidityInSeconds;
    }

    // 토큰 생성
    public String createToken(String username, Set<String> authorities) {
        String strAuthorities = String.join(" ", authorities);

        Instant now = Instant.now();

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plusSeconds(this.tokenValidityInSeconds))
                .subject(username)
                .claim(AUTHORITIES_KEY, strAuthorities)
                .build();
        return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}

AccessTokenProvider는 TokenProvider를 상속받으며 아이디, 비밀번호를 이용해 토큰을 생성하는 역할을 수행합니다.

시큐리티 설정 추가

JwtAuthenticationEntryPoint

AuthenticationEntryPoint는 인증 실패 시 동작하도록 시큐리티 설정파일 작성 시 지정할 예정입니다. 상속받아 구현합니다.

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

JwtAccessDeniedHandler

AccessDeniedHandler는 권한 체크 후 인가 실패 시 동작하도록 시큐리티 설정파일에 설정할 예정입니다.
AccessDeniedHandler를 상속받아 구현합니다.

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

PasswordEncoderConfig 작성

@Configuration
public class PasswordEncoderConfig {

    // BCryptPasswordEncoder 라는 패스워드 인코더 사용
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

유저의 비밀번호는 비밀번호가 저장된 DB를 열어볼 수 있는 개발자조차 몰라야 합니다. 이를 위해 유저의 비밀번호는 암호화하여 DB에 저장합니다.
비밀번호 암호화, 검증을 위해 PasswordEncoder 빈을 생성합니다.

SecurityConfig 작성

@Configuration
@EnableWebSecurity // 기본적인 웹보안을 활성화하겠다
@EnableMethodSecurity // @PreAuthorize 어노테이션 사용을 위해 선언
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf(AbstractHttpConfigurer::disable) // token을 사용하는 방식이기 때문에 csrf를 disable
                .formLogin(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .sessionManagement(httpSecuritySessionManagementConfigurer ->
                        httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .cors(httpSecurityCorsConfigurer ->
                        httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource())
                )
                .exceptionHandling(exceptionConfig ->
                        exceptionConfig.authenticationEntryPoint(jwtAuthenticationEntryPoint)
                                .accessDeniedHandler(jwtAccessDeniedHandler)
                )
                .headers(headerConfig -> headerConfig.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .authorizeHttpRequests(registry ->
                        registry.requestMatchers("/h2/**").permitAll()
                                .requestMatchers("/favicon.ico").permitAll()
                                .requestMatchers("/error").permitAll()

                )
                .authorizeHttpRequests(registry ->  // actuator, swagger 경로, 실무에서는 상황에 따라 적절한 접근제어 필요
                        registry.requestMatchers("/actuator/**").permitAll()
                                .requestMatchers("/docs/**").permitAll()
                )
                .authorizeHttpRequests(registry -> // api path
                        registry.requestMatchers("/api/hello").permitAll()
                                .requestMatchers("/api/v1/accounts/token").permitAll() // login
                                .requestMatchers("/api/v1/members").permitAll()
                )
                .authorizeHttpRequests(registry -> registry.anyRequest().authenticated()) // 나머지 경로는 jwt 인증 해야함
                .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer ->
                        httpSecurityOAuth2ResourceServerConfigurer.jwt(jwtConfigurer ->
                                jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter())
                        )
                );

        return httpSecurity.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(new CustomJwtGrantedAuthoritiesConverter());
        return converter;
    }

    private CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");

        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

@EnableMethodSecurity@PreAuthorize 어노테이션 사용을 위해 선언합니다.
@EnableWebSecurity는 기본적인 웹보안을 활성화하겠다는 어노테이션입니다.

header에서 Authorization 필드의 Bearer 토큰을 꺼내와 Authentication 객체를 만들어어 주는 부분은 oauth2-resource-server 의존성을 추가하고 oauth2ResourceServer를 지정해줌으로써 많은 추상화가 이루어졌습니다.

JwtAuthenticationConverter는 Jwt를 인증으로 변환하는 역할을 담당합니다.
우리는 인증 인가 후 만들어진 Authentication 객체를 통해 유저의 인증, 인가 정보를 가져와 사용할 수 있습니다.
혹시 토큰에서 authorities 정보를 꺼내와 객체를 구성하는 로직을 커스텀하고 싶을 수 있습니다. CustomJwtGrantedAuthoritiesConverter 클래스를 구성하고 JwtAuthenticationConverter에서 이를 지정합니다.

CustomJwtGrantedAuthoritiesConverter 작성

// 커스텀이 필요하면 수정
public class CustomJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

    @Override
    public Collection<GrantedAuthority> convert(Jwt source) {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        return converter.convert(source);
    }
}

현재는 단순 예제이므로 기본적인 JwtGrantedAuthoritiesConverter 객체를 생성해 conveter로 지정하고 JwtAuthenticationConverter를 반환했습니다.
앞으로 커스텀이 필요한 경우 해당 클래스를 수정해줍니다.

액세스토큰 인증 API 구현

DTO 생성

	@Builder
    public record TokenRequestDto(
            @NotNull
            @Size(min = 3, max = 50)
            String username,
            @NotNull
            @Size(min = 5, max = 100)
            String password
    ) {
    }
	@Builder
    public record TokenResponseDto(
            String accessToken,
    ) {
    }

repository 생성

public interface AccountRepository extends JpaRepository<Account, Long> {
    @EntityGraph(attributePaths = "authorities") // 엔티티그래프 통해 EAGER로 가져온다.
    Optional<Account> findOneWithAuthoritiesByUsername(String username); // user를 기준으로 유저를 조회할 때 권한정보도 가져온다.
}
public interface AuthorityRepository extends JpaRepository<Authority, String> {
}

service 구현

@Service
public class AccountService {
    private final AccessTokenProvider accessTokenProvider;
    private final RefreshTokenProvider refreshTokenProvider;
    private final AccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;

    // username 과 패스워드로 사용자를 인증하여 액세스토큰과 리프레시 토큰을 반환한다.
    @Override
    public TokenResponseDto authenticate(TokenRequestDto tokenRequestDto) {
        Account account = accountRepository.findOneWithAuthoritiesByUsername(tokenRequestDto.username())
                .orElseThrow(() -> new RuntimeException());

        if (!passwordEncoder.matches(tokenRequestDto.password(), account.getPassword())) {
            throw new RuntimeException();
        }
        if(!account.isActivated()) {
            throw new RuntimeException();
        }

        Set<String> authorities = account.getAuthorities().stream()
                .map(Authority::getAuthorityName).collect(Collectors.toSet());
        String accessToken = accessTokenProvider.createToken(account.getUsername(), authorities);

        return TokenResponseDto.builder()
                .accessToken(accessToken)
                .build();
    }
}

Controller 구현

@RestController
@RequestMapping("/api")
public class AccountController {
    private final AccountService accountService;

    // 생성자주입
    public AuthController(AccountService accountService) {
        this.accountService = accountService;
    }

    @PostMapping("/authenticate") // Account 인증 API
    public ResponseEntity<TokenResponseDto> authorize(@Valid @RequestBody TokenRequestDto requestDto) {

        TokenResponseDto token = accountService.authenticate(requestDto);

        // response header 에도 넣고 응답 객체에도 넣는다.
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Authorization", "Bearer " + token.accessToken());

        return new ResponseEntity<>(token, httpHeaders, HttpStatus.OK);
    }
}    

유저 등록 API 구현

ROLE_MEMBER을 부여하여 Account를 생성하는 유저 등록 API를 구현하겠습니다.

DTO 생성

    @Builder
    public record RegisterMemberRequestDto(
            @NotNull
            @Size(min = 3, max = 50)
            String username,

            @NotNull
            @Size(min = 5, max = 100)
            String password,

            @NotNull
            @Size(min = 5, max = 100)
            String nickname
    ) {
    }

AccountService 멤버 생성 메서드 구현

@Service
@RequiredArgsConstructor
public class AccountService {
    private final AccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;

......
....

    @Transactional
    @Override
    public void registerMember(RegisterMemberRequestDto requestDto) {
        Optional<Account> accountOptional = accountRepository.findOneWithAuthoritiesByUsername(requestDto.username());

        if (accountOptional.isPresent()) {
            throw new RuntimeExceptionn("이미 가입되어있는 유저");
        }

        // 이 유저는 권한이 MEMBER
        // 이건 부팅 시 data.sql에서 INSERT로 디비에 반영한다. 즉 디비에 존재하는 값이여야함
        Authority authority = Authority.builder()
                .authorityName("ROLE_MEMBER")
                .build();

        Account user = Account.builder()
                .username(requestDto.username())
                .password(passwordEncoder.encode(requestDto.password()))
                .nickname(requestDto.nickname())
                .authorities(Collections.singleton(authority))
                .activated(true)
                .build();

        accountRepository.save(user);
}    

ROLE_MEMBERAuthority 테이블에 존재하는 값이여야 합니다.
현재는 data.sql에서 애플리케이션 부팅 시 insert 해줍니다.

멤버 파사드 구현

AccountService를 가져다 사용하기 위해 파사드 패턴을 사용합니다.

@Builder
public record RegisterMemberFacadeRequestDto(
        @NotNull
        @Size(min = 3, max = 50)
        String username,

        @NotNull
        @Size(min = 5, max = 100)
        String password,

        @NotNull
        @Size(min = 5, max = 100)
        String nickname
) {
}
@Service
@RequiredArgsConstructor
public class MemberFacadeService {

    private final AccountService accountService;

    @Override
    public void signup(RegisterMemberFacadeRequestDto requestDto) {
        accountService.registerMember(RegisterMemberRequestDto.builder()
                        .nickname(requestDto.nickname())
                        .username(requestDto.username())
                        .password(requestDto.password())
                .build());
    }
}

controller 생성

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class MemberController {
    private final MemberFacadeService memberFacadeService;

    // user 등록 API
    @PostMapping("/members")
    public ResponseEntity<Void> signup(
            @Valid @RequestBody RegisterMemberFacadeRequestDto requestDto
    ) {
        memberFacadeService.signup(requestDto);

        return ResponseEntity
                .noContent()
                .build();
    }
}

권한 테스트

HelloController를 작성하여 인증과 권한에 따른 인가가 잘 작동하는지 테스트해봅니다. 먼저 AccountService에 기능을 몇 개 추가하겠습니다.

service

// AccountService
@Override
    @Transactional(readOnly = true)
    public AccountInfoResponseDto getAccountWithAuthorities(String username) {
        Account account = accountRepository.findOneWithAuthoritiesByUsername(username)
                .orElseThrow(() -> new RuntimeException());
        return AccountInfoResponseDto.of(account);
    }

위의 메서드는 username을 받아 해당 Account의 정보를 반환합니다.

HelloController

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class HelloController {

    private final AccountService accountService;

    @GetMapping("/hello")
    public ResponseEntity<String> hello() {
        return ResponseEntity.ok("hello");
    }

    // redirect test
    @PostMapping("/test-redirect")
    public void testRedirect(HttpServletResponse response) throws IOException {
        response.sendRedirect("/api/user");
    }

    // 인가 테스트
    // Authorization: Bearer {AccessToken}
    @GetMapping("/user")
    @PreAuthorize("hasAuthority('SCOPE_ROLE_MEMBER') or hasAuthority('SCOPE_ROLE_ADMIN')")
    @SecurityRequirement(name = "bearer-key")
    public ResponseEntity<AccountInfoResponseDto> getMyUserInfo(Authentication authentication) {
        log.info(authentication.getName());
        log.info(authentication.getAuthorities().toString());
        return ResponseEntity.ok(accountService.getAccountWithAuthorities(authentication.getName()));
    }

    @GetMapping("/user/{username}")
    @PreAuthorize("hasAuthority('SCOPE_ROLE_ADMIN')") // ADMIN 권한만 호출 가능
    @SecurityRequirement(name = "bearer-key")
    public ResponseEntity<AccountInfoResponseDto> getUserInfo(@PathVariable(name = "username") String username) {
        return ResponseEntity.ok(accountService.getAccountWithAuthorities(username));
    }
}

전체 예제 보기

https://github.com/suhongkim98/spring-security-jwt-ssongplate
위 깃허브를 통해 예제를 확인할 수 있습니다.

블로그에서는 액세스토큰만 설명드렸지만 예제에는 리프레시토큰 예제도 포함되어 있으며 계속 리팩토링되고 있는 레포지토리입니다.

profile
鈍筆勝聰✍️

3개의 댓글

comment-user-thumbnail
2023년 2월 9일

잘 봤습니다. 다만 궁금한 점이 있어서 댓글 남겨요!

제가 알기로 필터에서는 사용자의 인가처리보다는 잘못된 요청이나 비정상적인 사용자 접근을 처리해주는게 맞다고 생각했습니다. 하지만 코드를 보다보니, 필터에서 인가처리를 해주는 것 같아서 궁금해서 여쭤봅니다.

제가 스프링 시큐리티를 아직 사용해보지 않았기 때문에 그럴수도 있어서 혹시 스프링 시큐리티를 사용하면 필터에서 처리를 해줘야하는 건가요? 아니라면 인터셉터가 아닌 필터에서 인가 처리를 하신 이유가 있을까요?

2개의 답글