将@RolesAllowed 注释应用于存储在数据库中的现有用户

Posted

技术标签:

【中文标题】将@RolesAllowed 注释应用于存储在数据库中的现有用户【英文标题】:Apply @RolesAllowed annotations to existing users stored in database 【发布时间】:2018-03-03 17:56:36 【问题描述】:

TLDR 我开发的 Java Web 应用程序需要实现对 REST 服务的用户区分。我知道有一些注解(@RolesAllowed@PermitAll@DenyAll),可以描述哪个角色可以使用该服务。我的问题是,如何将@RolesAllowed 中的角色与用户角色枚举匹配,该用户是存储在数据库中的持久对象??

许多教程解释了@RolesAllowed,但我发现没有一个与这些角色相匹配,与已经创建的角色相匹配


说明

如何通过自动检查他的角色(通​​过会话 ID 找到)来验证用户?我知道泽西岛已经通过注册RolesAllowedDynamicFeature.class 做到了这一点。我已经设法通过将@DenyAll 注释放入方法中来检查此RolesAllowedDynamicFeature.class 是否有效,并返回403 错误。

首先,让我们从什么是用户开始。用户是 DB 实体,使用 Ebean 将它们从 Java 对象持久化到 Java 对象。这是来自User.class 的示例:

/*
* A sample that describes the fields that I ask for and to understand the concept
*/
@Entity
@Table(name = "users")
public class User extends Model

   @Id
   @GeneratedValue
   @Column(name = "id")
   private long id;

   @Enumerated(EnumType.STRING)
   @Column(name = "role", nullable = false)
   private UserRole role;

   // contructors getters setters helper methods etc

   /**
    * Fetch a user from DB
    *
    * @param id the id to search for
    * @return a Person.class object or may return null
    */
   public static User getUserById(Long id)
   
      return Ebean.find(User.class, id);
   

   /* and here is the UserRole enum that define the roles every user can have */
   public static enum UserRole
   
      Administrator, User, Manager;
   

以上所有代码,工作正常,用户存储正确,我可以轻松获取它们。

每个用户在登录时都会通过类似的服务进行身份验证,并为每个用户创建一个具有唯一会话 ID(使用 UUID)的 ConnectedUser 对象。 每次服务调用后,都会运行一个身份验证,验证用户是否可以通过搜索使用此服务,是否有此会话的连接用户条目(存储为 cookie):

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter

   @Context
   private HttpServletRequest request;

   /**
    * Authenticates a user's access with every request that is made via a token.
    *
    * @param requestContext The request that is sent to the server.
    * @throws IOException
    */
   @Override
   public void filter(ContainerRequestContext requestContext) throws IOException
   
      boolean isValidated;

      Cookie sessionCookie = null;
      Cookie[] cookies = request.getCookies();

      if (cookies.length != 0) 
         for (Cookie cookie : cookies) 
            if (cookie.getName().equals("CookieName")) 
               sessionCookie = cookie;
            
         
      

      if (sessionCookie != null) 
         // UserValidationHandler checks if user is in connected_users table
         isValidated = UserValidationHandler.validateUser(sessionCookie.getValue(), request.getRemoteAddr());
      
      else 
         MultivaluedMap pathParameters = requestContext.getUriInfo().getQueryParameters();

         // UserValidationHandler checks if user is in connected_users table
         isValidated = UserValidationHandler.validateUser((String)pathParameters.getFirst("token"), request.getRemoteAddr());
      

      if (!isValidated) 
         LOGGER.warn("[Authorization filter] Unauthorized user.");
         URI indexURI = URI.create("http://login.jsp");
         requestContext.setRequestUri(indexURI);
      
   


注意事项:

注意1:大多数实现建议将角色应用到 web.xml 文件中。尽管我认为,就我而言,这是不可行的。

注意2:另外,授权用户使用服务的正确位置在哪里?我发现我可以用@Priority(Priorities.AUTHORIZATION) 创建ContainerRequestFilter 类。哪个更好?

我有点迷路了。我阅读了许多 Q/A 或示例,但没有任何解释透彻。

【问题讨论】:

是的,您应该实现 ContainerRequestFilter 并将您的授权代码放在那里。您还应该创建一个实现 SecurityContext 的类以及一个实现 Principal 的类。有时间我会贴一些代码。 @dsp_user 我会看看你说的(据我了解,“是”指的是 Note2)。如果您设法在某个时候发布代码示例,将不胜感激!谢谢 是的,我关于实现 ContainerRequestFilter 的评论参考了注释 #2。就像我说的那样,我将发布一些代码,因为它可能仍然不清楚它是如何协同工作的。 【参考方案1】:

您应该将关注点分开并在一个过滤器中进行身份验证,在一个过滤器中进行授权。通常这是通过在身份验证过滤器中设置SecurityContext,然后从授权过滤器中检索它来完成的。

SecurityContext 具有您覆盖的 isUserInRole 方法。应该在授权过滤器中调用此方法。通常,您将拥有SecurityContext 成员的角色,并且只需迭代角色

static class MySecurityContext implements SecurityContext 
    private final String[] userRoles;

    public MySecurityContext(String[] roles, String user) 
        this.userRoles = roles;
    

    @Override
    public Principal getUser() 
        return new Principal() 
            @Override
            public String getName() 
                return name;
            
        
    

    public boolean isUserInRole(String role) 
        for (String userRole: userRoles) 
            if (role.equals(userRole) 
                return true;
            
         
        return false;
    

    // more methods to override

在身份验证过滤器中,您只需调用 requestContext.setSecurityContext 方法并传入新的 SecurityContext

在授权过滤器中,您将使用ResourceInfo 获得@RolesAllowed 注释。例如

class AuthorizationFilter implement ContainerRequestContext 
    @Context
    private ResourceInfo info;

    @Override
    public void filter(ContainerRequestContext request) 
        SecurityContext sc = request.getSecurityContext();
        RolesAllowed anno = info.getResourceMethod().getAnnotation(RolesAllowed.class);
        String rolesAllowed = anno.value();
        for (role: rolesAllowed) 
            if (sc.isUserInRole(role)) 
                 return;
            
        
        request.abortWith(Response.status(403).build());
    

或者类似的东西。

如果您使用的是 Jersey2,则不需要自己实施授权。这已经在RolesAllowedDynamicFeature 中实现。您只需要在您的应用程序中注册该功能。它的工作原理与我之前提到的相同; SecurityContext 预计会在到达授权过滤器之前被填充,过滤器将检查角色并授权或拒绝。

【讨论】:

这很有意义......我已经设法在我的应用程序中添加了RolesAllowedDynamicFeature。但是,这个类怎么可能自动匹配用户的角色(正如我所说的在数据库中存储为 varchars)和@RolesAllowed?另外,正如我从您的概念中了解的那样,请求首先从Authentication filter 传递,然后从Authorization filter 传递。但是——“A SecurityContext 预计在到达授权过滤器之前被填充”——实际上是什么意思?从一个过滤器传递到另一个过滤器时,我必须将这个上下文传递到响应/请求? RolesAllowedDynamicFeature 附带的授权过滤器将尝试从ContainerRequestContext 获取SecurityContext,然后获取您在@RolesAllowed 注释中指定的角色,然后检查这些角色SecurityContext 通过调用SecurityContextisUserInRole。我在上面展示了一个例子。如您所见,我只是允许通过 SecurityContext 构造函数传入滚动。 身份验证过滤器将在授权过滤器之前调用,因此在身份验证过滤器中,您只需在@987654349 上调用setSecurityContext 即可设置SecurityContext @。在认证过滤器中,通常是怎么做的,就是认证的时候,需要从数据库中对用户信息进行分级,所以这个时候也要抓取用户角色。在您确定它是一个有效用户后,您将使用这些角色填充 SecurityContext 并在请求上下文中设置安全上下文。 哇! .setSecurityContext() 是我错过的。我也没有听说过我们通常在身份验证期间从数据库中获取对象的细节。这真的澄清了这个过程!谢谢你的时间。我会尝试实现它,并且可能会起作用。否则,我稍后再打扰你。【参考方案2】:

我的实现与 peeskillet 的不同,但它对我有用。

实现安全性的代码可能看起来像这样

public class ServiceFilter implements ContainerRequestFilter 

    @Override
    public void filter(ContainerRequestContext req) throws IOException 

        //user name and password are obtained from the header
        String auth = req.getHeaderString(HttpHeaders.AUTHORIZATION);

        if(auth == null) 
            throw new WebApplicationException(Status.UNAUTHORIZED);
        

        //user name and password
        String[] credentials = auth.substring(1, auth.length()-1).split(":");

        String user = credentials[0];//user name
        String password = credentials[1];//password

        if(user == null || password == null)
             throw new WebApplicationException(Status.UNAUTHORIZED);

        ServiceSecurity ss = null;
        //user name and password are hardcoded here but you better put them in a DB or file
        if(user.equals("servUser") && password.equals("service"))
         ss = new ServiceSecurity(new ServiceUser("servUser"));
        else if(user.equals("servAdmin") && password.equals("admin"))
            ss = new ServiceSecurity(new ServiceUser("servAdmin"));
        else
             throw new WebApplicationException(Status.UNAUTHORIZED);

        req.setSecurityContext(ss);

    

安全上下文

import java.security.Principal;

import javax.ws.rs.core.SecurityContext;

public class ServiceSecurity implements SecurityContext 

    private ServiceUser sUser;
    public ServiceSecurity(ServiceUser sUser)
        this.sUser = sUser;
    

    @Override
    public String getAuthenticationScheme() 
        // TODO Auto-generated method stub
        return SecurityContext.DIGEST_AUTH;
    

    @Override
    public Principal getUserPrincipal() 
        // TODO Auto-generated method stub
        return sUser;
    

    @Override
    public boolean isSecure() 
        // TODO Auto-generated method stub
        return false;
    

    @Override
    public boolean isUserInRole(String role) 
        // TODO Auto-generated method stub
        return sUser.getRole().equals(role) ? true : false;
    


安全上下文实现类使用一个实现Principal (ServiceUser)的类进行初始化,该类用于获取该特定用户的角色。

import java.security.Principal;

public class ServiceUser implements Principal 

    private String role;

    public ServiceUser(String role)
        this.role = role;
    
    public ServiceUser()

    
    public String getRole()
        return role;
    
    public void setRole(String role)
        this.role = role;
    
    public String getName()
        return "some name";
    

在这种情况下,您需要将以下内容添加到您的服务器方法中

@RolesAllowed("servUser")

凭据(用户名和密码)应在请求标头中提供(从上面的过滤方法中可以看出)

【讨论】:

感谢您的建议!但正如我所见,在ServiceFilter 中,您从请求的标头中获取用户名和密码。我需要在为我的应用程序调用的每个服务中进行此过滤,并且不可能每次都向用户请求用户名和密码,也不能手动将它们添加到每个 ajax 请求中。有什么解决办法吗? 在这种情况下,您可能必须使用一些会话跟踪或其他提供状态的方式(就登录而言)。通常,REST 是无状态的,但如果需要,您仍然可以提供状态信息。因此,只对用户进行身份验证,然后设置一个会话变量(例如 myServLogin=true)。然后对于后续请求,检查此会话变量是否为真。当然,还有其他方法可以做到这一点。 哦,我明白你的意思了。我在登录时验证每个用户。否则他将根本无法调用任何服务。但我的需要是,对于到达服务器的每个请求,我需要检查发出请求的用户是否可以使用当前服务。例如,管理员和用户都在登录时进行身份验证。但是管理员可以调用比简单用户更多的服务。当页面打开(管理员或用户)时,它会向服务器发送一些 ajax 请求。我的限制级别是每个服务,我不希望用户能够调用管理员的服务。不过,感谢您抽出时间来回答。 通过会话(会话是每个用户,而不是每个请求),您可以实现所有这些。

以上是关于将@RolesAllowed 注释应用于存储在数据库中的现有用户的主要内容,如果未能解决你的问题,请参考以下文章

使用@RolesAllowed 和@PreAuthorize 保护控制器方法

如何在 ContainerRequest 中获取会话对象以使用注解 @RolesAllowed(Role_user)?

使用 Login Module JBoss Approach 成功登录后,无法(拒绝访问响应)执行带有注释 @RolesAllowed 的 rest 端点

@RolesAllowed() 在 keycloak 中检查啥角色

@RolesAllowed、@DenyAll 在 Web 层中的使用?

将 @PostFilter 注释应用于通用 Spring Data Jpa 存储库方法