Spring Security 将jwt刷新标记限制为仅限一个端点

k3fezbri  于 2022-12-13  发布在  Spring
关注(0)|答案(1)|浏览(148)

我已经从Spring资源服务器依赖关系中实现了JWT令牌授权和身份验证。

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig {

    @Value("${app.chat.jwt.public.key}")
    private RSAPublicKey publicKey;

    @Value("${app.chat.jwt.private.key}")
    private RSAPrivateKey privateKey;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.exceptionHandling(
                exceptions ->
                        exceptions
                                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                                .accessDeniedHandler(new BearerTokenAccessDeniedHandler()));

        http.authorizeHttpRequests()
                .requestMatchers("/auth/sign-in").permitAll()
                .requestMatchers("/auth/sign-up").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic(Customizer.withDefaults())
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);

        return http.build();
    }

    @SneakyThrows
    @Bean
    public JwtEncoder jwtEncoder() {
        var jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
        var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }

    @SneakyThrows
    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        var jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source =
                new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

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

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

它运行良好。我有AuthController,在其中实现了用于登录、注册和刷新令牌的端点。在每个端点中,我返回一个带有访问令牌和刷新令牌的响应。下面是控制器:

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final JwtTokenService tokenService;
    private final AuthenticationManager authManager;
    private final UserDetailsService usrDetailsService;

    private final UserService userService;


    record LoginRequest(String username, String password) {}
    @PostMapping("/sign-in")
    public TokensResponse login(@RequestBody LoginRequest request) {

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(request.username, request.password);
        authManager.authenticate(authenticationToken);

        var user = (User) usrDetailsService.loadUserByUsername(request.username);
        String accessToken = tokenService.generateAccessToken(user);
        String refreshToken = tokenService.generateRefreshToken(user);

        return new TokensResponse(accessToken, refreshToken);
    }

    record SignUpRequest(String username, String password){}

    @PostMapping("/sign-up")
    public TokensResponse signUp(@RequestBody SignUpRequest signUpRequest) {
        User registeredUser = userService.register(new AuthRequestDto(signUpRequest.username(), signUpRequest.password()));

        String accessToken = tokenService.generateAccessToken(registeredUser);
        String refreshToken = tokenService.generateRefreshToken(registeredUser);

        return new TokensResponse(accessToken, refreshToken);
    }

    @PreAuthorize("hasRole('REFRESH_TOKEN')")
    @GetMapping("/token/refresh")
    public TokensResponse refreshToken(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");
        String previousRefreshToken = headerAuth.substring(7);

        String username = tokenService.parseToken(previousRefreshToken);
        var user = (User) usrDetailsService.loadUserByUsername(username);
        String accessToken = tokenService.generateAccessToken(user);
        String refreshToken = tokenService.generateRefreshToken(user);

        return new TokensResponse(accessToken, refreshToken);
    }

    record TokensResponse(String accessToken, String refreshToken) {}

}

下面是TokenService类,我在其中生成这些令牌:

@Service
@RequiredArgsConstructor
public class JwtTokenServiceImpl implements JwtTokenService {

    private final JwtEncoder jwtEncoder;

    @Override
    public String generateAccessToken(User user) {
        Instant now = Instant.now();
        String scope = user.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(2, ChronoUnit.MINUTES))
                .subject(user.getUsername())
                .claim("scope", scope)
                .build();
        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

    @Override
    public String generateRefreshToken(User user) {
        Instant now = Instant.now();
        String scope = "ROLE_REFRESH_TOKEN";

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(10, ChronoUnit.MINUTES))
                .subject(user.getUsername())
                .claim("scope", scope)
                .build();
        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

    @Override
    public String parseToken(String token) {
        try {
            SignedJWT decodedJWT = SignedJWT.parse(token);
            return decodedJWT.getJWTClaimsSet().getSubject();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}

我想做的是限制刷新令牌仅用于刷新端点。因为如果可以对所有端点使用刷新令牌,那么拥有短期实时访问令牌又有什么意义呢?我已经尝试为刷新令牌提供REFRESH_TOKEN范围,并为刷新令牌端点添加了@PreAuthorize("hasRole('REFRESH_TOKEN')")注解。但它不起作用(我仍然可以发送访问令牌来刷新端点并获取新令牌),因为Spring不会从令牌中查找声明,他只是根据令牌中的用户名从数据库中加载用户,并在此处检查其角色。
请建议我如何才能使刷新令牌仅限于一个端点。也将是伟大的,使他一次性使用,但似乎我需要存储令牌的地方。

wrrgggsh

wrrgggsh1#

the refresh token is bound to the
   client to which it was issued.

来源https://www.rfc-editor.org/rfc/rfc6749#section-6
刷新令牌影响客户端范围(这意味着所有终结点)。因此,您的预期不可行。

相关问题