This is the third day of the Java EE Advent Calendar. Yesterday, HASUNUMA Kenji's [Presentation] Introduction to JCA and MDB (ja) was.
Well, Java EE 8 has finally been released this year! Is CDI 2.0 or Servlet 4 the highlight? I'm personally interested in this area because someone will explain it Java EE Security API (JSR 375) -spec.html) I would like to talk about.
The sample used this time can be obtained from the following. https://github.com/koduki/example-javaee8-security_basic
The Java EE Security API (JSR 375), as the name implies, is a specification that makes security, especially authentication and authorization, simpler and more portable. Most web applications have login capabilities and account access control. It doesn't matter who makes the functions around here, so I want FW to do it, right? With Rails, Devise provides such a function, and PHP's Symfony was incorporated as a standard function of FW.
However, although JACC, JASPIC, and vendor-specific implementations have existed in JavaEE for some time, they are complicated and there are few documents due to their own specifications, so the reality is that most people have implemented their own authentication / authorization. I suspect there isn't. In addition, since security was implemented independently by various components such as Servlet, JSF, CDI, EJB, etc., there was no specification for integrated management of it.
Taking these circumstances into consideration, JSR375 was created with reference to OSS libraries such as Apache Shiro with the keywords "unified management", "simple", and "portable". It is a specification.
The main points are the following three points.
--Unified access to each component --Access control by annotation --Unified access to authentication function
First, a Security Context was introduced to unify the authentication functions that were previously independent of each other. For example, until now, there were the following methods for acquiring an account for each component.
Access to these is summarized in the SecurityContext. The method I / F of each existing process remains the same, but the backend etc. seems to have changed. Also, Java EE 8 is totally integrated into CDI, so even if it's a serve red, for example.
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String webName = request.getUserPrincipal().getName();
The part that confirmed the login user as
public class MyServlet extends HttpServlet {
@Inject
private SecurityContext securityContext;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String webName = securityContext.getCallerPrincipal().getName();
And can also be taken via SecurityContext.
Access control can be easily performed by annotation. For example, a servlet that can only be accessed by users with the foo role can be created as follows.
@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class Servlet extends HttpServlet {
If Authentication Mechanism described later is set properly, you can jump to the login page by just setting the above, and you can easily define the flow after authentication or error.
The third point is unified access to the authentication function. We provide this in the form of an Identity Store. You can use DB, LDAP, or custom authentication function.
New web applications will often have an RDB as the back end, and enterprise systems will often use LDAP such as Active Directory. You can use these two just by writing the settings with annotations. Since EL expressions can be used for annotations, not only hard coding but also modern usage such as acquisition from configuration files and environment variables can be done without problems.
Also, you often want to use your own authentication API or standard authentication APIs such as OAuth and SAML. Even in that case, you can handle it by creating a custom Identity Store or Authentication Mechanism. In fact, the article "OpenID Connect with Java EE 8 Security API" There is also a case where OpenID Connect is supported, which is authenticated with relatively complicated transitions. It seems that the in-house API can usually be supported by referring to this. As mentioned in the article, I would like support for OAutht and other general authentication functions as standard or close to it.
Although the explanation is a little duplicated, the following three components are mainly used directly.
Authentication Mechanism
The Authentication Mechanism controls the authentication and subsequent control flow. It seems that BASIC authentication will be performed with @BasicAuthenticationMechanismDefinition, which is mainly provided, or HttpAuthenticationMechanism will be inherited and custom forms will be used. By combining HttpAuthenticationMechanism and @LoginToContinue etc., I feel that general login control can be created quickly.
The simple in-house system and API are BASIC authentication, and if you need a login screen, you can use a custom form.
Identity Store
The Identity Store provides the ability to authenticate.
For example, if you want the backend to be an RDB, you can use @DatabaseIdentityStoreDefinition and define it as follows:
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "${Config.getDataSourceName()}", // "java:global/MyAppDataSource"
callerQuery = "select password from caller where name = ?",
groupsQuery = "select group_name from caller_groups where caller_name = ?",
hashAlgorithm = Pbkdf2PasswordHash.class,
priorityExpression = "#{100}",
hashAlgorithmParameters = {
"Pbkdf2PasswordHash.Iterations=3072",
"${applicationConfig.dyna}"
} // just for test / example
)
Since you can also write an EL expression in the annotation, you can also create a configuration file or a class to be read from the outside and get the configuration value from it as in this example.
SecurityContext
SecurityContext is a mechanism for unified access to the above two functions. As mentioned above, the methods of accessing the authentication mechanism are diverse and complicated. SecurityContext makes it easy to:
--getCallerPrincipal: Get the Caller (user). Get as a String with getName --isCallerInRole: Check if the role passed as an argument has a logged-in one
Let's show a super-simple implementation example. This time with BASIC authentication.
First, install GlassFish 5 and work with NetBeans. If you create a Maven project with NetBeans 8.2, it will be EE7, so let's modify it to EE8. By the way, Java is also specified in 1.8.
@@ -18,7 +18,7 @@
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
- <version>7.0</version>
+ <version>8.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
@@ -30,8 +30,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
- <source>1.7</source>
- <target>1.7</target>
+ <source>1.8</source>
+ <target>1.8</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
First, make a target serve red. I will omit the description, but also create beans.xml and ApplicationConfig.java empty
@WebServlet("/myservlet")
public class MyServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Inject
private SecurityContext securityContext;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("This is a servlet \n");
String webName = null;
if (securityContext.getCallerPrincipal() != null) {
webName = securityContext.getCallerPrincipal().getName();
}
response.getWriter().write("web username: " + webName + "\n");
response.getWriter().write("web user has role \"admin\": " + securityContext.isCallerInRole("admin") + "\n");
response.getWriter().write("web user has role \"users\": " + securityContext.isCallerInRole("users") + "\n");
response.getWriter().write("web user has role \"guest\": " + securityContext.isCallerInRole("guest") + "\n");
}
}
When accessed, the result will be as follows.
$ curl -L http://localhost:8080/example-javaee8-security_basic/myservlet
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 137 100 137 0 0 9133 0 --:--:-- --:--:-- --:--:-- 9133
This is a servlet
web username: null
web user has role "admin": false
web user has role "users": false
web user has role "guest": false
First, make this page accessible only to Admin. Specify @ServletSecurity for the serve red to limit the role.
@WebServlet("/myservlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class MyServlet extends HttpServlet {
.
.
.
If you try to access it again, you can see that it resulted in a 401 error.
$ curl -IL http://localhost:8080/example-javaee8-security_basic/myservlet
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 1090 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
HTTP/1.1 401 Unauthorized
Server: GlassFish Server Open Source Edition 5.0
Since you cannot access it as it is, add BASIC authentication. Add @BasicAuthenticationMechanismDefinition to ApplicationConfig.java.
@BasicAuthenticationMechanismDefinition(
realmName = "test realm"
)
@ApplicationScoped
@Named
public class ApplicationConfig {
}
We will also create an Identity Store that will be the back end for the Basic Authentication Mechanism Definition.
@ApplicationScoped
public class TestIdentityStore implements IdentityStore {
public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
if (usernamePasswordCredential.compareTo("admin", "password")) {
return new CredentialValidationResult("admin", new HashSet<>(asList("admin", "users")));
}
return INVALID_RESULT;
}
}
The password is also a hard-coded super-simple specification. A good girl shouldn't imitate in production, right? Let's deploy and run it.
$ curl -LI -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 137 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition 5.0
$ curl -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 137 100 137 0 0 4419 0 --:--:-- --:--:-- --:--:-- 8562
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false
You can see that the result is returned properly without becoming 401. You can see the result of securityContext properly.
Hard coding is not so much, so let's change the backend to RDB. First of all, you need a database to use. If you are using GlassFish5, Derby will be included, so use that. Initialize the DB with the following code and create a data source.
Originally, you should make a management tool, but this time the password is also hashed and stored. I also changed the password from the previous one to make it easier to verify.
@DataSourceDefinition(
name = "java:global/MyAppDataSource",
minPoolSize = 0,
initialPoolSize = 0,
className = "org.apache.derby.jdbc.ClientDataSource",
user = "APP",
password = "APP",
databaseName = "myapp",
properties = {"connectionAttributes=;create=true"}
)
@Singleton
@Startup
public class DatabaseSetup {
@Resource(lookup = "java:global/MyAppDataSource")
private DataSource dataSource;
@Inject
private Pbkdf2PasswordHash passwordHash;
@PostConstruct
public void init() {
Map<String, String> parameters = new HashMap<>();
parameters.put("Pbkdf2PasswordHash.Iterations", "3072");
parameters.put("Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512");
parameters.put("Pbkdf2PasswordHash.SaltSizeBytes", "64");
passwordHash.initialize(parameters);
// executeUpdate(dataSource, "DROP TABLE caller");
// executeUpdate(dataSource, "DROP TABLE caller_groups");
executeUpdate(dataSource, "CREATE TABLE caller(name VARCHAR(64) PRIMARY KEY, password VARCHAR(255))");
executeUpdate(dataSource, "CREATE TABLE caller_groups(caller_name VARCHAR(64), group_name VARCHAR(64))");
executeUpdate(dataSource, "INSERT INTO caller VALUES('admin', '" + passwordHash.generate("secret1".toCharArray()) + "')");
executeUpdate(dataSource, "INSERT INTO caller_groups VALUES('admin', 'admin')");
}
@PreDestroy
public void destroy() {
try {
executeUpdate(dataSource, "DROP TABLE caller");
executeUpdate(dataSource, "DROP TABLE caller_groups");
} catch (Exception ex) {
ex.printStackTrace();
// silently ignore, concerns in-memory database
}
}
private void executeUpdate(DataSource dataSource, String query) {
try (Connection connection = dataSource.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(query)) {
statement.executeUpdate();
}
} catch (SQLException ex) {
throw new IllegalStateException(ex);
}
}
}
Next, delete the TestIdentityStore created earlier and add @DatabaseIdentityStoreDefinition to ApplicationConfig.java instead.
@BasicAuthenticationMechanismDefinition(
realmName = "test realm"
)
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "java:global/MyAppDataSource",
callerQuery = "select password from caller where name = ?",
groupsQuery = "select group_name from caller_groups where caller_name = ?",
hashAlgorithm = Pbkdf2PasswordHash.class,
priorityExpression = "#{100}",
hashAlgorithmParameters = {
"Pbkdf2PasswordHash.Iterations=3072",
"${applicationConfig.dyna}"
} // just for test / example
)
@ApplicationScoped
@Named
public class ApplicationConfig {
public String[] getDyna() {
return new String[]{"Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512", "Pbkdf2PasswordHash.SaltSizeBytes=64"};
}
}
The data source used is the one created in DatabaseSetup.java earlier. The password hash algorithm also uses PBKDF2 so that the received password has the same hash value as that registered in the DB.
I will try this.
$ curl -u admin:secret1 http://localhost:8080/example-javaee8-security_basic/myservlet
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 137 100 137 0 0 4419 0 --:--:-- --:--:-- --:--:-- 4419
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false
Did you authenticate with the new password registered in the DB? Since the back end of the authentication mechanism is separated in this way, the implementation can be easily changed.
There are many places I haven't investigated yet, but I tried to touch Java EE Security API (JSR 375). It was. If it is a simple API authentication function, it is convenient because it seems that even today's contents can be created quickly.
In the future, I would like to try more practical parts such as LDAP linkage and custom form linkage to make an article.
Then Happy Hacking!
Recommended Posts