Java EE 7 Aggregate Apache Shiro

Apache Shiro is a powerful and easy-to-use Java security framework, its main functions are authentication (authentication), authentication (authorization), encryption and session management. With the help of Shiro's easy-to-use API, we can quickly and easily secure any application - from the smallest mobile applications to large web enterprise applications.

Security is a must when we build enterprise web applications with Java EE. Security is a broad concept. In our web applications, there are mainly the following aspects:

  • Authentication (authentication): determine whether "you" is "you", and the related action is login;
  • Authentication (authorization): determine whether "you" can perform certain operations;
  • Encryption: Generally speaking, it is the encrypted storage of user login password;

A series of functions provided by Shiro are meeting the security requirements of Java EE applications. Here I will introduce how to use Shiro to "protect" our Java EE web applications. I will use Wildfly 10 as the application server for Java EE web applications, and maven as the project build tool. Let's take a look at the specific steps below (the source code can be found on my github ):

add dependencies

<dependency>
  <groupId>javax.enterprise</groupId>
  <artifactId>cdi-api</artifactId>
  <version>1.2</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>jaxrs-api</artifactId>
  <version>3.0.10.Final</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.3.2</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-web</artifactId>
  <version>1.3.2</version>
</dependency>

I introduce it here cdi-api, because we will use the interceptor (Interceptor) later to realize the parsing RequiresRolesof annotations; we will demonstrate how to protect the external interface of the application in the form of a REST interface, so I introduce it here jaxrs-api; the next is a related dependency of Shiro.

A simple web application

We use jaxrs-apito implement a simple web application, first to configure jaxrs:

src/main/java/com/github/holyloop/rest/config/JaxRsActivator.java:

@ApplicationPath("/api")
public class JaxRsActivator extends Application {
    /* class body intentionally left blank */
}

This is the REST interface path of $context-root/api/*our .

Add a few simple REST interfaces, start by adding an interface that anyone can access:

src/main/java/com/github/holyloop/rest/controller/AnonController.java:

@Path("/anon")
public class AnonController {
    @GET
    @Path("/greet")
    @Produces(MediaType.APPLICATION_JSON)
    public Response anyoneCanAccess() {
        return Response.status(Status.OK).entity("hello").build();
    }
}

Add one more protected interface (of course, it is not protected until Shiro is configured):

src/main/java/com/github/holyloop/rest/controller/SecuredController.java:

@Path("/secured")
public class SecuredController {
    @GET
    @Path("/root")
    @Produces(MediaType.APPLICATION_JSON)
    public Response roleRootNeeded() {
        return Response.status(Status.OK).entity("hello, i'm secured").build();
    }
}

Placement Shiro

For the sake of convenience, the definitions of users and roles are placed in the configuration file. In the next article, I will explain how to configure database-based user/role/authority management.

src/main/webapp/WEB-INF/shiro.ini:

# =============================================================================
# Apache Shiro INI 配置
# =============================================================================

[main]
# authc 是 org.apache.shiro.web.filter.authc.FormAuthenticationFilter 的缩写
authc.loginUrl = /login

# -----------------------------------------------------------------------------
# 用户和他们的角色(可选的)
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest

# -----------------------------------------------------------------------------
# 角色和其被授予的权限
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *

# -----------------------------------------------------------------------------
# 资源和filter chains
# _URL_Ant_Path_Expression_ = _Path_Specific_Filter_Chain_
# -----------------------------------------------------------------------------
[urls]
/index.html = anon
/api/anon/** = anon
/login = authc
/api/secured/** = authc

First, the value of the filter configured in the [main]configuration block isauthc one of several default filters provided by Shiro. The definitions of other filters can be referred to here ; then we configure users and roles in and blocks respectively; resource paths and corresponding filters.loginUrl/loginauthc[users][roles][urls]

It should be noted that the URL is a path relative to the application root path (context-root). If our application was initially www.somehost.com/myappchanged to for www.anotherhost.comsome , the url configuration here is still valid; another important point is the URL The order of definitions is important, one criterion is " FIRST MATCH WINS" (FIRST MATCH WINS), for example we
define:

/account/** = ssl, authc
/account/signup = anon

Then there is the problem: if a request wants to access /account/signup/index.html(which should be allowed to any user), it will never be processed, because by matching that path /account/**first
, it "short-circuits" the rest of the matching definition.

Load Shiro configuration

web.xmlWe load the Shiro we just configured with :

src/main/webapp/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="3.0"
  xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <session-config>
    <session-timeout>30</session-timeout>
  </session-config>

  <!-- listener -->
  <!-- EnvironmentLoaderListener 默认情况下使用 /WEB-INF/shiro.ini (或classpath:shiro.ini 
    作为配置文件 -->
  <listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
  </listener>

  <!-- filter -->
  <!-- ShiroFilter 使用 EnvironmentLoaderListener 初始化的 WebEnvironment 做必要的安全操作 -->
  <filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
  </filter-mapping>

</web-app>

Deployment test

Next, deploy the application to Wildfly (or an application server such as Glassfish that is compatible with Java EE). In the example, the application is deployed to port 8080 of the local machine; here I use curl to test whether Shiro is working properly:

direct interview

First, we request the two previously defined REST interfaces separately without logging in:

  • Any user-accessible /anon/greetinterface :
curl -i localhost:8080/shiro-basic/api/anon/greet


shiro-basicIs the application context (context-root), you can see that the response is 200, and the response body is "hello"

  • /secured/rootInterfaces that require login to access :
curl -i localhost:8080/shiro-basic/secured/root


You can see a redirected response, which is what we configured. It /api/secured/** = authcworked . An unauthenticated user requested an interface that requires authentication. Shiro helped us redirect the request to the login location (see the Set-Cookieitem in the response header), which is we configured authc.loginUrl.

Log in

shiro.iniWe configured in authc.loginUrl = /login, authc is a form authentication filter, so the login operation is:

curl -i -d "username=guest&password=guest" -c cookies "localhost:8080/shiro-basic/login"


The Set-Cookiefield specifies the session ID (JSESSIONID item) of this session. You can look at the cookie file specified in the curl command (compare the value of the Set-Cookie field in the response header with the content of the cookies file):

After logging in, With a valid cookie, we are the user who has passed the Shiro authentication, and can normally request the interface that requires authentication:

curl -i -b cookies localhost:8080/shiro-basic/api/secured/root

Annotation-based authentication

In actual project development, we often use annotations to judge roles and permissions, such as:

@RequiresRoles("admin")
public void foo() {}

This means that foo()the execution requires the requester to have the "admin" role, and this class of annotations also supports logical operations, such as:

@RequiresRoles(value = {"admin","sa"}, logical = Logical.OR)

In order for @RequiresRoles, @RequiresPermissionsetc. to take effect, we need to borrow an interceptor (Interceptor) to help us complete the parsing work.

Defining the ShiroSecured annotation

First we customize a Shiro-specific interceptor binding:

src/main/java/com/github/holyloop/interceptor/ShiroSecured.java:

@InterceptorBinding
@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface ShiroSecured {}

Then implement the interceptor:

src/main/java/com/github/holyloop/interceptor/ShiroSecuredInterceptor.java:

@Interceptor
@ShiroSecured
public class ShiroSecuredInterceptor {

    @AroundInvoke
    public Object interceptShiroSecurity(InvocationContext context) throws Exception {
        Class<?> c = context.getTarget().getClass();
        Method m = context.getMethod();
        Subject subject = SecurityUtils.getSubject();

        if (!subject.isAuthenticated() && hasAnnotation(c, m, RequiresAuthentication.class)) {
            throw new UnauthenticatedException("Authentication required");
        }

        if (subject.getPrincipal() != null && hasAnnotation(c, m, RequiresGuest.class)) {
            throw new UnauthenticatedException("Guest required");
        }

        if (subject.getPrincipal() == null && hasAnnotation(c, m, RequiresUser.class)) {
            throw new UnauthenticatedException("User required");
        }

        RequiresRoles roles = getAnnotation(c, m, RequiresRoles.class);

        if (roles != null) {
            // logic and
            if (Logical.AND.equals(roles.logical())) {
                subject.checkRoles(Arrays.asList(roles.value()));
            }
            // logic or
            if (Logical.OR.equals(roles.logical())) {
                boolean hasAtLeastOneRole = false;
                for (String role : roles.value()) {
                    if (subject.hasRole(role)) {
                        hasAtLeastOneRole = true;
                        break;
                    }
                }
                if (!hasAtLeastOneRole) {
                    throw new AuthorizationException();
                }
            }
        }

        RequiresPermissions permissions = getAnnotation(c, m, RequiresPermissions.class);

        if (permissions != null) {
            // logic and
            if (Logical.AND.equals(permissions.logical())) {
                subject.checkPermissions(permissions.value());
            }
            // logic or
            if (Logical.OR.equals(permissions.logical())) {
                boolean hasAtLeastOnePermission = false;
                for (String permission : permissions.value()) {
                    if (subject.isPermitted(permission)) {
                        hasAtLeastOnePermission = true;
                        break;
                    }
                }
                if (!hasAtLeastOnePermission) {
                    throw new AuthorizationException();
                }
            }
        }

        return context.proceed();
    }

    /**
     * 被调用方法是否有指定的注解
     * 
     * @param c 被调用方法所属的类
     * @param m 被调用方法
     * @param a 目标注解
     * @return
     */
    private static boolean hasAnnotation(Class<?> c, Method m, Class<? extends Annotation> a) {
        return m.isAnnotationPresent(a) || c.isAnnotationPresent(a) || c.getSuperclass().isAnnotationPresent(a);
    }

    /**
     * 获取被调用方法指定的注解详情
     * 
     * @param c 被调用方法所属的类
     * @param m 被调用方法
     * @param a 目标注解
     * @return
     */
    private static <A extends Annotation> A getAnnotation(Class<?> c, Method m, Class<A> a) {
        return m.isAnnotationPresent(a) ? m.getAnnotation(a)
                : c.isAnnotationPresent(a) ? c.getAnnotation(a) : c.getSuperclass().getAnnotation(a);
    }
}

Note the few authentication/authentication failure exceptions I throw here, we'll deal with them later.

Next, we need to explicitly enable the interceptor:

src/main/webapp/WEB-INF/beans.xml:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
  bean-discovery-mode="all">

    <interceptors>
        <class>com.github.holyloop.interceptor.ShiroSecuredInterceptor</class>
    </interceptors>
    
</beans>

Handling authentication exceptions

In our interceptor, depending on the situation will throw AuthorizationExceptionor UnauthenticatedException, representing unauthorized and unauthenticated, respectively, usually corresponding to 403 and 401 respectively in HTTP response codes. Here we need to handle these two exceptions and encapsulate the verbose exception information into a message that the caller can understand.

src/main/java/com/github/holyloop/rest/config/RestUnauthorizedExceptionMapper.java:

@Provider
public class RestUnauthorizedExceptionMapper implements ExceptionMapper<AuthorizationException> {

    @Override
    public Response toResponse(AuthorizationException arg0) {
        return Response.status(Status.FORBIDDEN)
                .entity(new ExceptionMessageWrapper(403, "forbidden"))
                .build();
    }

}

src/main/java/com/github/holyloop/rest/config/RestUnauthenticatedExceptionMapper.java:

@Provider
public class RestUnauthenticatedExceptionMapper implements ExceptionMapper<UnauthenticatedException> {

    @Override
    public Response toResponse(UnauthenticatedException arg0) {
        return Response.status(Status.UNAUTHORIZED)
                .entity(new ExceptionMessageWrapper(401, "unauthorized"))
                .build();
    }

}

Among them ExceptionMessageWrapperis a custom exception message wrapper, optional (if you don't need it, let the entity() of the response be empty), its structure is very simple:

private int code;
private String message;

public ExceptionMessageWrapper(int code, String message) {
  super();
  this.code = code;
  this.message = message;
}

Application Authentication Annotation

Next, we simply apply the authentication annotation and modify src/main/java/com/github/holyloop/rest/controller/SecuredController.javait to :

@ShiroSecured
@Path("/secured")
public class SecuredController {

    @RequiresRoles("admin")
    @GET
    @Path("/root")
    @Produces(MediaType.APPLICATION_JSON)
    public Response roleRootNeeded() {
        return Response.status(Status.OK).entity("hello, i'm secured").build();
    }

}

We added it on the class declaration @ShiroSecuredso that all methods of the class will be ShiroSecuredInterceptorprocessed roleRootNeededon @RequiresRoles("admin")the method, indicating that the caller of the method needs to have the admin role.

Deployment test

First request the interface with a user without the admin role:

curl -i -d "username=guest&password=guest" -c cookies "localhost:8080/shiro-basic/login"
curl -i -b cookies localhost:8080/shiro-basic/api/secured/root

The results are as follows:

you can see that the response is 403; then switch to the user test with the admin role:

curl -i -d "username=root&password=secret" -c cookies "localhost:8080/shiro-basic/login"
curl -i -b cookies localhost:8080/shiro-basic/api/secured/root

The result is as follows:

then Shiro's authentication annotation takes effect.

Summarize

The above is the basic method of integrating Shiro in the Java EE environment. We shiro.inihave configured a form-based authentication filter in authc; added several hard-coded users and roles; added the interceptor of Shiro authentication annotations, making authentication More flexible and convenient. In the next article, I will introduce the Shiro authentication/authentication configuration method based on the database.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324738340&siteId=291194637