使用 JWT 身份验证与用户合作的正确方法是啥?

Posted

技术标签:

【中文标题】使用 JWT 身份验证与用户合作的正确方法是啥?【英文标题】:What's a correct way to work with users using JWT authentication?使用 JWT 身份验证与用户合作的正确方法是什么? 【发布时间】:2021-07-30 02:05:23 【问题描述】:

我使用 REST API 和 JWT 身份验证/授权为我的移动应用程序创建了后端。 然后我使用 Retrofit 创建了 android 应用程序。 从 /login 端点检索 JWToken 后,我在服务器端创建了 GET 请求,以使用令牌解析当前登录用户的用户名,然后在客户端调用方法。

UserController.java(服务器端

@RestController
@RequestMapping(path = "/user")
public class UserController 

    private UserService userService;

    public UserController(UserService userService) 
        this.userService = userService;
    

    @GetMapping
    public String getCurrentUser(@AuthenticationPrincipal Object user) 

        user = SecurityContextHolder.getContext().getAuthentication()
                .getPrincipal();

        return user.toString();
    

但我不确定这样开发它是否正确。

假设我的数据库中有两个表。

一个带有用于身份验证的登录凭据 其次是用户个人数据

现在我想显示用户的名字和姓氏。

现在,登录后我拥有的唯一信息是他登录时使用的用户名,如果我想获得更多信息,我必须以某种方式在客户端进行查询:

首先 - 获取用户 ID,其中 username = 我从令牌中获得的用户名 然后 - 从第一个查询中获取 users_data 其中 user_id = id 的对象

我认为这个过程不应该在客户端完成(如果我错了,请纠正我)。

问题

所以我的问题是我应该怎么做才能实现这种情况,我想获取有关用户的所有信息,而我在客户端应用程序中只有他的用户名。我应该在后端进行更改,还是坚持通过移动应用进行查询?

(服务器端)

AuthenticationFilter.java

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    private AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) 
        this.authenticationManager = authenticationManager;
    

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

        // Mapping credentials to loginviewmodel
        LoginViewModel credentials = null;
        try 
            credentials = new ObjectMapper().readValue(request.getInputStream(), LoginViewModel.class);
         catch (IOException e) 
            e.printStackTrace();
        

        // Creating login token
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                credentials.getUsername(),
                credentials.getPassword(),
                new ArrayList<>()
        );

        // Authenticate user
        Authentication auth = authenticationManager.authenticate(authenticationToken);
        return auth;
    

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authResult
    ) throws IOException, ServletException 

        // Grab current user
        UserImpl principal = (UserImpl) authResult.getPrincipal();

        // Create JWT Token
        String token = JWT.create()
                .withSubject(principal.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
                .sign(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()));

        // Add token in response(this is syntax of token)
        response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + token);

    

AuthorizationFilter.java

public class JwtAuthorizationFilter extends BasicAuthenticationFilter 

    private UserRepository userRepository;

    public JwtAuthorizationFilter(
            AuthenticationManager authenticationManager,
            UserRepository userRepository
    ) 
        super(authenticationManager);
        this.userRepository = userRepository;
    

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

        // Read authorization header with JWT Token
        String header = request.getHeader(JwtProperties.HEADER_STRING);

        if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) 
            chain.doFilter(request, response);
        

        // Try get user data from DB to authorize
        Authentication authentication = getUsernamePasswordAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        chain.doFilter(request, response);
    

    private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) 

        String token = request.getHeader(JwtProperties.HEADER_STRING);

        if (token != null) 

            // parse and validate token
            String username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()))
                    .build()
                    .verify(token.replace(JwtProperties.TOKEN_PREFIX, ""))
                    .getSubject();

            if (username != null) 

                User user = userRepository.findByUsername(username);
                UserImpl principal = new UserImpl(user);
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, principal.getAuthorities());
                return auth;
            
            return null;
        
        return null;
    

UserImpl.java

public class UserImpl implements UserDetails 

    private User user;

    public UserImpl(User user) 
        this.user = user;
    

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 

        List<GrantedAuthority> authorities = new ArrayList<>();

        // Get list of roles (ROLE_name)
        this.user.getRoleList().forEach( role -> 
            GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
            authorities.add(authority);
        );

        return authorities;
    

    @Override
    public String getPassword() 
        return this.user.getPassword();
    

    @Override
    public String getUsername() 
        return this.user.getUsername();
    

(客户端)

从当前登录用户解析用户名的方法:

public void getCurrentUser() 

        Call<String> call = ApiClient.getUserService(getApplicationContext()).getCurrentUser();
        call.enqueue(new Callback<String>() 
            @Override
            public void onResponse(Call<String> call, Response<String> response) 

                if (response.isSuccessful()) 

                    String user = response.body();
                    nameOfUserView.setText(user);
                
            

            @Override
            public void onFailure(Call<String> call, Throwable t) 
                nameOfUserView.setText(t.getMessage());
            
        );

【问题讨论】:

【参考方案1】:

你的逻辑有漏洞。让我们看看

#1 - 没关系。基于 JWT 令牌,您正在获取用户名 #2 - 在标头中使用令牌,您可以通过发送用户名来获取其他详细信息。

在#2 中,如果我发送任何其他用户的用户名而不是登录用户怎么办。系统仍会回复详细信息。因此,任何登录的用户都可以看到任何用户的详细信息。

要处理这个问题,您应该使用一些具有所有必需字段的 DTO 类,例如 UserReturnData。结构将是

public class UserReturnData 

    String username;
    List<String> roles;
    Long id;
//more fields as per requirement

然后在您当前的用户调用中,根据授权标头填充此数据。不要发送任何用户名。授权标头应该足以获取用户详细信息。示例:

public UserReturnData fetchUserDetails()
    
        UserReturnData userReturnData = new UserReturnData();
        List<String> roles = new ArrayList<String>();

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        userReturnData.setUsername(auth.getName());
        Long id = uRepo.findId(auth.getName());
        userReturnData.setId(id);
        Collection<SimpleGrantedAuthority> authorities = (Collection<SimpleGrantedAuthority>) SecurityContextHolder
                .getContext().getAuthentication().getAuthorities();
        for (SimpleGrantedAuthority authority : authorities)
        
            roles.add(authority.getAuthority());
        
        userReturnData.setRoles(roles);

//Populate other required fields here. 
        return userReturnData;
    

当您需要登录用户的详细信息时。您可以仅使用授权令牌调用当前用户 API 并获取登录用户信息

【讨论】:

以上是关于使用 JWT 身份验证与用户合作的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

JWT身份验证中刷新令牌的正确实现是啥

将 Firebase 身份验证与 Firestore 一起使用的正确方法是啥?

微服务方法之间的身份验证

php 后端实现JWT认证方法

php 后端实现JWT认证方法

JWT