【问题标题】:Using JWT with @PathVariable but only allow access url for spesific user将 JWT 与 @PathVariable 一起使用,但只允许特定用户的访问 url
【发布时间】:2020-12-20 10:59:53
【问题描述】:

我正在使用 Spring Boot 创建简单的 Rest 社交媒体应用程序。我在应用程序中使用 JWT 进行身份验证。

当用户注册时,在我的移动应用程序中,我从用户那里获取一些信息并创建用户的帐户和个人资料。 顺便说一下,您可以看到(简化的)帐户和配置文件的数据库对象。我使用 Mongo DB 作为数据库。

帐户: { “_id”:“b6164102-926e-47d8-b9ff-409c44dc47c0”, “电子邮件”:“xxx@yy.com” …… }

简介: { “_id”:“35b06171-c16a-4559-90f3-df81ace6d64a”, “accountId”: “b6164102-926e-47d8-b9ff-409c44dc47c0”, 个人资料图片:[ { “imageId”:“1431b0bc-feb7-436d-9d3a-7b9094547bf6”, “imageLink”:“https://this_is_some_link_to_image.com } …… ] …… }

当用户登录到应用程序时,我将 accountId 添加到 JWT,然后在我的移动应用程序中我调用下面的端点来获取用户的个人资料信息。我从 jwt 获取 accountId 并找到该帐户 ID 的配置文件。

@GetMapping("/profiles")

public ResponseEntity<BaseResponse> getUserProfile(@AuthenticationPrincipal AccountId accountId) {


    var query = new Query(accountId);

    var presenter = new GetUserProfilePresenter();



    useCase.execute(query, presenter);



    return presenter.getViewModel();

}

在应用程序中,用户可以使用以下端点将照片上传到他们的个人资料;

@PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

        @PathVariable("profileId") UUID profileId, @RequestParam("image") MultipartFile image) throws IOException {

    
     ......
 
}

一切正常,但问题是有人可以使用他们的令牌通过其他人的 profileId 调用此 url。因为 profileId 不是隐藏的 id。在我的移动应用中,用户可以使用以下网址随机播放并查看其他用户的个人资料。

任何经过身份验证的用户都可以访问此网址。

@GetMapping(path = "/profiles/{profileId}")

public ResponseEntity<BaseResponse> getProfile(@PathVariable("profileId") UUID profileId) {
                      
 ......
}

现在,我的问题是我怎样才能使 "/profiles/{profileId}/images" 这个网址只有此个人资料的用户才能访问,而无需更改路径格式。

例如;

用户 A - 个人资料 ID = XXX

用户 B - 个人资料 ID = YYY

我希望如果用户 A 使用自己的 JWT 令牌调用此 url,则仅将图像上传到自己的个人资料而不是另一个个人资料。

我想出了一些解决方案,但是这些解决方案导致我更改了 url 路径;

解决方案 1:

我可以在 jwt 中使用 accountId。使用此 accountId 查找用户的个人资料,以便每次调用此 url 保证仅将图像上传到令牌用户的个人资料。

但是这个解决方案会像下面这样更改 url 路径,因为我不需要从路径中获取任何 profileId。

@PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

           @AuthenticationPrincipal AccountId accountId, @RequestParam("image") MultipartFile image) throws IOException {


    ......
 
}

解决方案 2:

这与第一个解决方案非常相似,唯一不同的是我为用户创建 jwt 时。我将把用户的 profileId 放到 JWT 里面。因此,当用户调用 url 时,我将从 jwt 获取 profileId 并放入 Authentication 对象中。在控制器中,我将获取此 profileId 用于查找用户的个人资料,然后将图像上传到此特定的个人资料。 而且,这个解决方案改变了 url 路径格式,因为我不需要从 url 路径获取 profileId。

所以,如果我回到我的主要问题。这些问题和情况的最佳做法和解决方案是什么?

~~~编辑~~~

对于那些好奇的人,我没有改变我的道路。实际上,我实施了解决方案 1

现在我同时使用来自 JWT 的 accountId 和 profileId,所以当我想找到完全那个用户的个人资料时,我会同时使用 accountId 和 profileId 搜索数据库。

有了这个改变,我不需要改变其他路径。

例如; (GET) /profiles/{profileId} 此路径对所有经过身份验证的用户仍然有意义。

但是(POST)/profiles/{profileId}/images此路径仅对特定(令牌所有者)用户有意义。

顺便说一句,我为我的管理员角色操作以“api/admin/**”前缀开头的路径。

最终代码(控制器);

@PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
    @AuthenticationPrincipal AccountId accountId,
    @PathVariable("profileId") UUID profileId,
    @RequestParam("image") MultipartFile image) throws IOException {
        ....
    }

最终代码(存储库);

@Repository
public interface ProfileJpaRepository extends MongoRepository<ProfileDto, String> {
    Optional<ProfileDto> findByAccountId(String accountId);
    Optional<ProfileDto> findByIdAndAccountId(String profileId, String accountId);
}

【问题讨论】:

  • 搜索PermissionEvaluatorSpring@PreAuthorize("hasRole('....') and hasPermission('...') or ...")管理
  • @Zorglube 嗨。不幸的是角色不能解决我的问题。因为每个用户都有角色USER。我需要确保用户只能通过此 api 调用更改自己的个人资料。顺便说一句,您如何看待我的解决方案?
  • PermissionEvaluator@PreAuthorize("hasPermission('...')")

标签: spring spring-boot rest spring-mvc jwt


【解决方案1】:

处理这种情况的最佳做法是有两个端点,每个端点都需要不同类型的权限:

  • “/profiles/{profileId}/images”可供管理员使用,因此如果管理员想要更改其他用户的个人资料图片,他们可以通过调用此端点来实现。
  • “/profiles/images”将负责更改具有最低权限的最通用用户。

因此,在这两种情况下,您都需要从 JWT 中提取 AccountId,并且您不应该直接从用户那里获取 AccountId,除非出于管理目的检查授权用户的权限。

现在,实现这样一个系统的最佳方法是使用 Spring Security 并创建自定义 AuthenticationToken,然后自定义 AbstractUserDetailsAuthenticationProvider、* AbstractAuthenticationProcessingFilter* 和 用户名密码验证令牌。 完成后,您可以将 Spring 配置为使用自定义提供程序进行身份验证。

用户名密码认证令牌

public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;

public JwtAuthenticationToken(String jwtToken) throws Exception {
    super(null, jwtToken);
    // Verify JWT and get the payload
    this.payload = // set the payload
}

public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
    super(principal, authToken.getCredentials(), authorities);
    this.payload = authToken.payload;
    authToken.eraseCredentials(); // not sure if this is needed
}

public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    if (isAuthenticated) {
        throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    } else {
        super.setAuthenticated(false);
    }
}

public Payload getPayload() {
    return this.firebaseToken;
}

public boolean isCreationAllowed() {
    return creationAllowed;
}

public void setCreationAllowed(boolean creationAllowed) {
    this.creationAllowed = creationAllowed;
}
}

AbstractUserDetailsAuthenticationProvider

@Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

@Autowired
AppUserService appUserService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
            this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
    );

    JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;

    String principal;
    try {
        principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
    } catch (RuntimeException re) {
        throw new AuthenticationException("Could not extract user's email address.");
    }

    AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
    return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}

@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
    result.setDetails(user);
    return result;
}

@Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
    UserDetails userDetails = appUserService.loadUserByUsername(s);
    JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
    if (userDetails != null)
        return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
    else
        throw new AuthenticationException("Creating the user details is not allowed.");
}


@Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
    // Nothing to do
}

@Override
public boolean supports(Class<?> authentication) {
    return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}

}

AbstractAuthenticationProcessingFilter

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public JwtAuthenticationFilter() {
    super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}

@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    return true; // Here I am returning true to require authentication for all requests.
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

    String authorization = request.getHeader("Authorization");
    if (authorization == null || !authorization.startsWith("Bearer "))
        throw new AuthenticationException("No JWT token found in request headers");

    String authToken = authorization.substring(7);
    JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
    return getAuthenticationManager().authenticate(token);
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
    super.successfulAuthentication(request, response, chain, authResult);
    // Authentication process succeed, filtering the request in.
    // As this authentication is in HTTP header, after success we need to continue the request normally
    // and return the response as if the resource was not secured at all
    chain.doFilter(request, response);
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    super.unsuccessfulAuthentication(request, response, failed);
    // Authentication process failed, filtering the request out.
}
}

用户详情

public class AppUser implements UserDetails {
     // A class to be used as a container for user details, you can add more details specific to your application here.
}

最后,你需要配置 Spring boot 来使用这些类:

安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
        // -- public paths, for example: swagger ui paths
        new AntPathRequestMatcher("/swagger-ui.html"),
        new AntPathRequestMatcher("/swagger-resources/**"),
        new AntPathRequestMatcher("/v2/api-docs"),
        new AntPathRequestMatcher("/webjars/**")
);

private JwtAuthenticationProvider provider;

public SecurityConfig(JwtAuthenticationProvider provider) {
    this.provider = provider;
}

@Override
public void configure(final WebSecurity web) {
    web.ignoring()
            .antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
            .requestMatchers(PUBLIC_URLS);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .exceptionHandling()
            // this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
            .authenticationEntryPoint(forbiddenEntryPoint())
            .and()
            .authenticationProvider(this.provider)
            .addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
}


@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
    final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
    filter.setAuthenticationManager(this.authenticationManager());
    filter.setAuthenticationSuccessHandler(this.successHandler());
    filter.setAuthenticationFailureHandler(this.failureHandler());
    return filter;
}


@Bean
JwtAuthenticationSuccessHandler successHandler() {
    return new JwtAuthenticationSuccessHandler();
}

@Bean
JwtAuthenticationFailureHandler failureHandler() {
    return new JwtAuthenticationFailureHandler();
}

/**
 * Disable Spring boot automatic filter registration.
 */
@Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
    final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

@Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
    return new HttpStatusEntryPoint(FORBIDDEN);
}
}

AuthenticationFailureHandler

public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {

private ObjectMapper objectMapper = new ObjectMapper();

@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
    Map<String, Object> data = new HashMap<>();
    data.put("exception", e.getMessage());
    httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}

}

AuthenticationSuccessHandler

public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

}

}

好的! 现在您已经正确实现了安全性,您可以使用最后一部分从任何地方访问用户详细信息和权限:

用户详细信息服务

@Service
public class AppUserService implements UserDetailsService {

@Autowired
private AppUserRepository appUserRepository;

public AppUser getCurrentAppUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null)
        return (AppUser) authentication.getDetails();
    return null;
}

public String getCurrentPrincipal() {
    return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
    appUserOptional.ifPresent(AppUser::loadAuthorities);
    return appUserOptional.orElse(null);
}
}

太好了。 让我们看看如何在控制器中使用它:

@PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(@RequestParam("image") MultipartFile image) throws IOException {

    
    AppUser user = this.appUserService.getCurrentAppUser();
    Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
    // set the profile picture.
    // save changes of repository and return.
}

出于管理目的:

@PreAuthorize ("hasRole('ROLE_ADMIN')")
@PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

    @PathVariable("profileId") UUID profileId, @RequestParam("image") MultipartFile image) throws IOException {

    
    AppUser user = this.appUserService.getCurrentAppUser();
    // set the profile picture using profileId parameter
    // save changes of repository and return.
}

剩下的唯一任务是在从数据库加载ROLE_ADMIN 时将其分配给正确的用户。为此,有很多不同的方法,这完全取决于您的要求。总的来说,您可以将角色保存在数据库中并将其与特定用户相关联,然后使用实体简单地加载它。

【讨论】:

  • 嘿,非常感谢您的所有努力 :) 实际上我做了一些与您非常相似的事情,但没有改变路径。
【解决方案2】:

让我们在这里做一些事情,我假设您有两个实体 - AccountProfile 并且您希望使用相同的 API 上传/更新新的个人资料图片 -

@PostMapping(path = "/profiles/{profileId}/images

如果 ADMIN 角色,更新 @PathVariable("profileId") 的个人资料图像,或者如果 USER 角色使用@PathVariable("profileId") 而不是任何其他Profile 实体图像更新他们自己的个人资料图像如果当前用户已通过身份验证,则使用 ProfileId

请查看此链接进行角色权限验证 Spring Boot : Custom Role - Permission Authorization using SpEL

用户主体

@Getter
@Setter
@Builder
public class UserPrincipal implements UserDetails {

/**
 * Generated Serial ID
 */
private static final long serialVersionUID = -8983688752985468522L;

private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Collection<? extends GrantedAuthority> permissions;

public static UserPrincipal createUserPrincipal(Account account) {
    if (userDTO != null) {
        List<GrantedAuthority> authorities = userDTO.getRoles().stream().filter(Objects::nonNull)
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());

        List<GrantedAuthority> permissions = account.getRoles().stream().filter(Objects::nonNull)
                .map(Role::getPermissions).flatMap(Collection::stream)
                .map(permission -> new SimpleGrantedAuthority(permissionDTO.getName().name()))
                .collect(Collectors.toList());

        return UserPrincipal.builder()
                .id(account.getId())
                .email(account.getEmail())
                .authorities(authorities)
                .permissions(permissions)
                .build();
    }
    return null;
}

身份验证过滤器

public class AuthTokenFilter extends OncePerRequestFilter {

@Autowired
private JwtUtils jwtUtils;

@Autowired
private CustomUserDetailsService customUserDetailsService;

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

        String jwtToken = getJwtTokenFromHttpRequest(request);

        if (StringUtils.isNotBlank(jwtToken) && jwtUtils.validateToken(jwtToken)) {
            Long accountId = jwtUtils.getAccountIdFromJwtToken(jwtToken);

            UserDetails userDetails = customUserDetailsService.loadUserByUserId(accountId);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails
                    .getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

    } catch (Exception exception) {

    }
    filterChain.doFilter(request, response);
}

private String getJwtTokenFromHttpRequest(HttpServletRequest request) {
    String bearerToken = request.getHeader("Authorization");
    if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {
        return bearerToken.substring(7, bearerToken.length());
    }
    return null;
 }
}

AuthUtil

@UtilityClass
public class AuthUtils {

  public boolean isAdmin(UserPrincipal userPrincipal){
    if(CollectionUtils.isNotEmpty(userPrincipal.getAuthorities())){
        return userPrincipal.getRoles().stream()
                .filter(Objects::nonNull)
                .map(GrantedAuthority::getName)
                .anyMatch(role -> role.equals("ROLE_ADMIN"));
    }
    return false;
  }
}

个人资料服务

@Service
public class ProfileService {

  @Autowired
  private ProfileRepository profileRepository;

  public Boolean validateProfileIdForAccountId(Integer profileId, Long accountId) throws NotOwnerException,NotFoundException {
    Profile profile = profileRepository.findByAccountId(profileId,accountId);
    if(profile == null){
        throw new NotFoundException("Profile does not exists for this account");
    } else if(profile.getId() != profileId){
        throw new NotOwnerException();
    }
    return true;
 }
}

配置文件控制器

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
@PostMapping(path = "/profiles/{profileId}/images", consumes = 
MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
    @AuthenticationPrincipal UserPrincipal currentUser,
    @PathVariable("profileId") UUID profileId,
    @RequestParam("image") MultipartFile image) throws IOException {

    if(!AuthUtils.isAdmin(currentUser)){
        profileService.validateProfileIdForAccountId(profileId, currentUser.getId());
    }
}

现在您可以验证 @PathVariable("profileId") 是否确实属于经过身份验证的 CurrentUser,您还可以检查 CurrentUser 是否为 ADMIN。

您还可以添加和检查角色的任何特定权限以方便上传/更新

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') or hasPermission('UPDATE')")

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多