Shiro-based security layer for AppBase projects.
- Provides a
UserStorefor holding registered users and associated permissions, optionally including password credentials. Includes a database-backed store based on embedded Derby and a memory based implementation which is loaded from a configuration file. - Provides a Realm implementation which has an associated
UserStoreand allows authentication tokens which are externally validated. - Permissions structure is based on Shiro Wildcard permissions but assumes a simplified pattern of
"{action}:{location}". Permissions can be retrieved by location as well as by user.
See the separate CHANGELOG.md.
We can use any Shiro configuration method and register an instance of AppBaseRealm with an associated UserStore.
The normal way to do this in a web app is to include Shiro in the web.xml and provide a shiro.ini file to customize the Shiro set up. To get access to the UserStore it can be preferable to create the UserStore as part of appbase config and then reference that from the shiro.ini. This is the approach outlined below.
There are three web.xml configuration directives needed to set up Shiro.
(Optional) Define a filter to enable Shiro-based control of page accesses:
<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>Set a listener to startup the Shiro environment on webapp start up. Put this after the appbase listener:
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>Minimal Shiro configuration goes in WEB-INF/shiro.ini
# =======================
# Shiro INI configuration
# =======================
[main]
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager
realm = com.epimorphics.appbase.security.AppRealm
realm.discoverUserStore = userstore
realm.authenticationCachingEnabled = true
securityManager.realms = $realm
[users]
[roles]
[urls]This configures the appbase Realm and attaches a user credentials store which is found as the appbase component called "userstore".
The [users] and [roles] sections aren't used.
For a web application you can also control authentication here, for example:
# =======================
# Shiro INI configuration
# =======================
[main]
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager
realm = com.epimorphics.appbase.security.AppRealm
realm.discoverUserStore = userstore
realm.authenticationCachingEnabled = true
securityManager.realms = $realm
passAuth = org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter
passAuth.loginUrl = /view/login-page
[users]
[roles]
[urls]
/view/login-page = anon
/view/** = passAuth
/api/** = passAuth
/system/security/login = anon
/system/** = passAuthIn the WEB-INF/app.conf file, create and configure an appropriate UserStore.
For a Derby database store, use something like:
userstore = com.epimorphics.appbase.security.DBUserStore
userstore.initfile = {webapp}/WEB-INF/user.ini
userstore.dbfile = /var/opt/ldregistry/dcutil/userstore
userstore.systemHome = /var/opt/ldregistry/dcutil/The dbfile parameter gives the location where the database will be stored. The systemHome parameter is home the home directory where Derby will put things like log files.
For an in-memory store, use:
userstore = com.epimorphics.appbase.security.MemUserStore
userstore.initfile = {webapp}/WEB-INF/user.iniThe initfile parameter gives optional initial user credentials information. The layout of this file is described below.
The user store, whether memory based or full DB, can be preloaded from an initialization file.
By convention, this is user.ini.
The user.ini file comprises a set of declarations, one per line. Lines beginning with # are comment lines.
User registration entries start with the user keyword and take the form:
user openid "name"or
user id "name" passwordFor example:
user dave@epimorphics.com "Dave Reynolds" shouldbechangedThere is a built-in anonymous user with pseudo id of http://localhost/anon.
It is convenient to declare that user in the initialization file as well, so as to bind a
visible name to that id. For example:
user http://localhost/anon "Any authenticated"
The second type of declaration line is used to grant permissions to a user. These take the form:
id permission
Where the permission takes the form:
action:target
Each part (action or target) can be a comma-separated list of values or a wildcard (*).
For example:
http://localhost/anon Read:*
Provide a set of API endpoints for login, logout and any required user management. For example:
@Path("login")
@POST
public Response login(
@FormParam("userid") String userid,
@FormParam("password") String password,
@FormParam("rememberMe") boolean rememberMe,
@FormParam("redirectURL") String redirectURL) {
if (Login.passwordLogin(userid, password, rememberMe)) {
log.info("User " + userid + " logged in");
if (validator != null) {
validator.successfulLogin(userid);
}
return redirectTo(redirectURL);
} else {
return redirectToView("login-page?error=Login+credentials+failed");
}
}
@Path("logout")
@POST
public Response logout() {
Subject subject = SecurityUtils.getSubject();
log.info("User " + subject.getPrincipal() + " logged out");
subject.logout();
return redirectToView("index");
}To check permissions in API endpoints, use the Shiro utilities. A convenient pattern is the throw an exception if a security restriction is violated and then use a mapper to catch the exception and render a message.
For example, to perform the checks you might use something like:
public void checkAllowed(String permission) {
Subject subject = SecurityUtils.getSubject();
if ( ! subject.isPermitted(permission) ) {
throw new SecurityViolationException(permission, (UserInfo) subject.getPrincipal());
}
}Then, somewhere visible to Jersey, declare a mapper:
@Provider
public class SecurityViolationMapper implements
ExceptionMapper<SecurityViolationException> {
static final Logger log = LoggerFactory.getLogger( SecurityViolationMapper.class );
@Override
public Response toResponse(SecurityViolationException exception) {
String permission = exception.getPermission();
log.warn( String.format(
"User %s blocked attempting action requiring permission %s",
exception.getUser(), permission) );
String message = "You do not have permission to " + permission;
String location = PubUtil.get().getViewBase() +
"/error?message=" + NameUtils.encodeSafeName(message);
try {
URI locationURI = new URI(location);
return Response.seeOther(locationURI).build();
} catch (URISyntaxException e) {
log.error("Internal error reporting security violation", e);
return null;
}
}
}The UserStore implementation also provides various methods for accessing available
permissions and users and for creating time-limited password credentials.
See the source code for details.