C'est le troisième jour du calendrier de l'avent Java EE. Hier, c'était [[Présentation] Introduction à JCA et MDB (ja)] de HASUNUMA Kenji (http://www.coppermine.jp/docs/notepad/2017/12/introduction-to-jca-and-mdb.html) était.
Eh bien, Java EE 8 est enfin sorti cette année! CDI 2.0 ou Servlet 4 est-il le point culminant? Je suis personnellement intéressé par ce domaine car quelqu'un vous l'expliquera API de sécurité Java EE (JSR 375) -spec.html) Je voudrais parler.
L'échantillon utilisé cette fois peut être obtenu à partir de ce qui suit. https://github.com/koduki/example-javaee8-security_basic
L'API de sécurité Java EE (JSR 375), comme son nom l'indique, est une spécification qui rend la sécurité, en particulier l'authentification et l'autorisation, plus simple et plus portable. La plupart des applications Web ont des capacités de connexion et un contrôle d'accès au compte. Peu importe qui fait les fonctions ici, alors je veux que FW le fasse, non? Avec Rails, Devise fournit une telle fonction, et Symfony de PHP a été incorporé en tant que fonction standard de FW.
Cependant, bien que les implémentations JACC, JASPIC et spécifiques au fournisseur existent dans JavaEE depuis longtemps, elles étaient compliquées et il y avait peu de documents en raison de leurs propres spécifications, donc la réalité est que la plupart des gens ont implémenté leur propre authentification / autorisation. Je soupçonne que non. De plus, comme la sécurité était implémentée indépendamment par divers composants tels que les servlets, JSF, CDI, EJB, etc., il n'y avait pas de spécification pour leur gestion intégrée.
Compte tenu de ces circonstances, JSR375 a été créé en référence à des bibliothèques OSS telles que Apache Shiro avec les mots-clés «gestion unifiée», «simple» et «portable». C'est une spécification.
Les principaux points sont les trois points suivants.
Tout d'abord, un contexte de sécurité a été introduit pour unifier les fonctions d'authentification qui étaient auparavant indépendantes pour chacune. Par exemple, jusqu'à présent, il existait les méthodes suivantes pour acquérir un compte pour chaque composant.
L'accès à ces derniers est résumé dans le SecurityContext. La méthode I / F existante de chaque processus reste la même, mais le backend etc. semble avoir changé. De plus, Java EE 8 est totalement intégré à CDI, donc même s'il s'agit d'un serveur rouge, par exemple.
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String webName = request.getUserPrincipal().getName();
La partie qui a confirmé l'utilisateur connecté comme
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();
Et peut également être pris via SecurityContext.
Le contrôle d'accès peut être facilement effectué par des annotations. Par exemple, un servlet auquel seuls les utilisateurs avec le rôle foo peuvent accéder peut être créé comme suit.
@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class Servlet extends HttpServlet {
Si le mécanisme d'authentification décrit plus loin est défini de manière appropriée, vous pouvez accéder à la page de connexion en définissant simplement ce qui précède, et vous pouvez facilement définir le flux après l'authentification ou lorsqu'une erreur se produit.
Le troisième point est l'accès unifié à la fonction d'authentification. Nous fournissons cela sous la forme d'un magasin d'identité. Vous pouvez utiliser la fonction d'authentification DB, LDAP ou personnalisée.
Les nouvelles applications Web auront souvent RDB comme back-end, et les systèmes d'entreprise utiliseront souvent LDAP tel qu'ActiveDirectory. Vous pouvez utiliser ces deux simplement en écrivant les paramètres avec des annotations. Étant donné que les expressions EL peuvent être utilisées pour les annotations, non seulement le codage en dur, mais également les utilisations modernes telles que l'acquisition à partir de fichiers de configuration et de variables d'environnement peuvent être effectuées sans problème.
En outre, vous souhaiterez souvent utiliser votre propre API d'authentification ou des API d'authentification standard telles que OAuth et SAML. Même dans ce cas, vous pouvez le gérer en créant un magasin d'identité ou un mécanisme d'authentification personnalisé. En fait, l'article "OpenID Connect with Java EE 8 Security API" Il existe également un exemple de prise en charge d'OpenID Connect, qui est authentifié avec des transitions relativement compliquées. Il semble que l'API interne puisse généralement être prise en charge en se référant à cela. Comme mentionné dans l'article, je souhaite une prise en charge d'OAutht et des fonctions générales d'authentification en standard ou à proximité.
Bien que l'explication soit un peu dupliquée, les trois composants suivants sont principalement utilisés directement.
Authentication Mechanism
Le mécanisme d'authentification contrôle l'authentification et le flux de contrôle ultérieur. Il semble que l'authentification BASIC sera effectuée avec @BasicAuthenticationMechanismDefinition, qui est principalement fourni, ou HttpAuthenticationMechanism sera hérité et des formulaires personnalisés seront utilisés. En combinant HttpAuthenticationMechanism et @LoginToContinue etc., je pense que le contrôle de connexion général peut être créé rapidement.
Le système interne simple et l'API sont une authentification BASIC, et si vous avez besoin d'un écran de connexion, vous pouvez utiliser un formulaire personnalisé.
Identity Store
Le magasin d'identité offre la possibilité de s'authentifier.
Par exemple, si vous souhaitez que le backend soit un RDB, vous pouvez utiliser @DatabaseIdentityStoreDefinition pour définir:
@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
)
Comme vous pouvez également écrire une expression EL dans l'annotation, vous pouvez également créer un fichier de paramètres ou une classe à lire de l'extérieur et en obtenir la valeur de paramètre comme dans cet exemple.
SecurityContext
SecurityContext est un mécanisme pour un accès unifié aux deux fonctions ci-dessus. Comme mentionné ci-dessus, les méthodes d'accès au mécanisme d'authentification sont diverses et compliquées. SecurityContext permet de:
--getCallerPrincipal: récupère l'appelant (utilisateur). Obtenir en tant que chaîne avec getName --isCallerInRole: vérifie si le rôle passé à l'argument en a un connecté
Montrons un exemple d'implémentation super simple. Cette fois avec la certification BASIC.
Tout d'abord, installez GlassFish 5 et travaillez avec NetBeans. Si vous créez un projet Maven avec NetBeans 8.2, ce sera EE7, modifions-le donc en EE8. À propos, Java est également spécifié dans la version 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>
Tout d'abord, faites servir une cible en rouge. Je vais omettre la description, mais aussi créer des beans.xml et ApplicationConfig.java vides
@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");
}
}
Lors de l'accès, le résultat sera le suivant.
$ 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
Tout d'abord, rendez cette page accessible uniquement à l'administrateur. Spécifiez @ServletSecurity pour le serveur rouge afin de limiter le rôle.
@WebServlet("/myservlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class MyServlet extends HttpServlet {
.
.
.
Si vous essayez à nouveau d'y accéder, vous pouvez voir que cela a entraîné une erreur 401.
$ 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
Puisque vous ne pouvez pas y accéder tel quel, ajoutez une authentification BASIC. Ajoutez @BasicAuthenticationMechanismDefinition à ApplicationConfig.java.
@BasicAuthenticationMechanismDefinition(
realmName = "test realm"
)
@ApplicationScoped
@Named
public class ApplicationConfig {
}
Nous allons également créer un magasin d'identités qui sera le back-end pour la définition du mécanisme d'authentification de base.
@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;
}
}
Le mot de passe est également une spécification ultra-simple codée en dur. Une bonne fille ne devrait pas imiter dans la production, non? Déployons et exécutons ceci.
$ 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
Vous pouvez voir que le résultat est renvoyé correctement sans devenir 401. Vous pouvez voir correctement le résultat de securityContext.
Le codage en dur n'est pas tellement, alors changeons le backend en RDB. Tout d'abord, vous avez besoin d'une base de données à utiliser. Si vous utilisez GlassFish5, Derby sera inclus, alors utilisez-le. Initialisez la base de données avec le code suivant pour créer une source de données.
À l'origine, vous devriez créer un outil de gestion, mais cette fois, le mot de passe est également haché et stocké. J'ai également changé le mot de passe du précédent pour le rendre plus facile à vérifier.
@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);
}
}
}
Ensuite, supprimez le TestIdentityStore créé précédemment et ajoutez à la place @DatabaseIdentityStoreDefinition à ApplicationConfig.java.
@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"};
}
}
La source de données utilisée est celle créée dans DatabaseSetup.java précédemment. L'algorithme de hachage de mot de passe utilise également PBKDF2 afin que le mot de passe reçu ait la même valeur de hachage que celle enregistrée dans la base de données.
J'essaierai ceci.
$ 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
Vous êtes-vous authentifié avec le nouveau mot de passe enregistré dans la base de données? Puisque le back-end du mécanisme d'authentification est séparé de cette manière, l'implémentation peut être facilement modifiée.
Il y a beaucoup d'endroits que je n'ai pas encore étudiés, mais j'ai essayé de toucher API de sécurité Java EE (JSR 375). C'était. S'il s'agit d'une simple fonction d'authentification API, c'est pratique car il semble que même le contenu d'aujourd'hui puisse être créé rapidement.
À l'avenir, j'aimerais essayer des parties plus pratiques telles que le lien LDAP et le lien de formulaire personnalisé pour créer un article.
Alors bon piratage!
Recommended Posts