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

Apply @RolesAllowed annotations to existing users stored in database

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

许多教程解释了 @RolesAllowed,但我发现 none 与已经创建的角色匹配


说明

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

首先,我们先从什么是用户说起。用户是数据库实体,使用 Ebean 将它们从 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 class。哪个比较好?

我有点迷路了。我读了很多 Q/As 或那里的样本,但没有任何解释透彻。

您应该将关注点分开,并在一个过滤器中执行身份验证,在一个过滤器中执行授权。通常这是如何实现的,方法是从身份验证过滤器内部设置 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
}

在身份验证过滤器中,您只需调用传入新 SecurityContextrequestContext.setSecurityContext 方法。

在授权过滤器中,您将通过使用 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 预计在到达授权过滤器之前填写,过滤器将检查角色并授权或拒绝。

我的实现与 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;
    }

}

安全上下文实现 class 使用 class 实现主体 (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")

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