Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
ModularRealmAuthenticator |
|
| 2.6;2.6 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one | |
3 | * or more contributor license agreements. See the NOTICE file | |
4 | * distributed with this work for additional information | |
5 | * regarding copyright ownership. The ASF licenses this file | |
6 | * to you under the Apache License, Version 2.0 (the | |
7 | * "License"); you may not use this file except in compliance | |
8 | * with the License. You may obtain a copy of the License at | |
9 | * | |
10 | * http://www.apache.org/licenses/LICENSE-2.0 | |
11 | * | |
12 | * Unless required by applicable law or agreed to in writing, | |
13 | * software distributed under the License is distributed on an | |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
15 | * KIND, either express or implied. See the License for the | |
16 | * specific language governing permissions and limitations | |
17 | * under the License. | |
18 | */ | |
19 | package org.apache.shiro.authc.pam; | |
20 | ||
21 | import org.apache.shiro.authc.*; | |
22 | import org.apache.shiro.realm.Realm; | |
23 | import org.apache.shiro.subject.PrincipalCollection; | |
24 | import org.apache.shiro.util.CollectionUtils; | |
25 | import org.slf4j.Logger; | |
26 | import org.slf4j.LoggerFactory; | |
27 | ||
28 | import java.util.Collection; | |
29 | ||
30 | /** | |
31 | * A {@code ModularRealmAuthenticator} delgates account lookups to a pluggable (modular) collection of | |
32 | * {@link Realm}s. This enables PAM (Pluggable Authentication Module) behavior in Shiro. | |
33 | * In addition to authorization duties, a Shiro Realm can also be thought of a PAM 'module'. | |
34 | * <p/> | |
35 | * Using this Authenticator allows you to "plug-in" your own | |
36 | * {@code Realm}s as you see fit. Common realms are those based on accessing | |
37 | * LDAP, relational databases, file systems, etc. | |
38 | * <p/> | |
39 | * If only one realm is configured (this is often the case for most applications), authentication success is naturally | |
40 | * only dependent upon invoking this one Realm's | |
41 | * {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} method. | |
42 | * <p/> | |
43 | * But if two or more realms are configured, PAM behavior is implemented by iterating over the collection of realms | |
44 | * and interacting with each over the course of the authentication attempt. As this is more complicated, this | |
45 | * authenticator allows customized behavior for interpreting what happens when interacting with multiple realms - for | |
46 | * example, you might require all realms to be successful during the attempt, or perhaps only at least one must be | |
47 | * successful, or some other interpretation. This customized behavior can be performed via the use of a | |
48 | * {@link #setAuthenticationStrategy(AuthenticationStrategy) AuthenticationStrategy}, which | |
49 | * you can inject as a property of this class. | |
50 | * <p/> | |
51 | * The strategy object provides callback methods that allow you to | |
52 | * determine what constitutes a success or failure in a multi-realm (PAM) scenario. And because this only makes sense | |
53 | * in a mult-realm scenario, the strategy object is only utilized when more than one Realm is configured. | |
54 | * <p/> | |
55 | * As most multi-realm applications require at least one Realm authenticates successfully, the default | |
56 | * implementation is the {@link AtLeastOneSuccessfulStrategy}. | |
57 | * | |
58 | * @see #setRealms | |
59 | * @see AtLeastOneSuccessfulStrategy | |
60 | * @see AllSuccessfulStrategy | |
61 | * @see FirstSuccessfulStrategy | |
62 | * @since 0.1 | |
63 | */ | |
64 | public class ModularRealmAuthenticator extends AbstractAuthenticator { | |
65 | ||
66 | /*-------------------------------------------- | |
67 | | C O N S T A N T S | | |
68 | ============================================*/ | |
69 | 1 | private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class); |
70 | ||
71 | /*-------------------------------------------- | |
72 | | I N S T A N C E V A R I A B L E S | | |
73 | ============================================*/ | |
74 | /** | |
75 | * List of realms that will be iterated through when a user authenticates. | |
76 | */ | |
77 | private Collection<Realm> realms; | |
78 | ||
79 | /** | |
80 | * The authentication strategy to use during authentication attempts, defaults to a | |
81 | * {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy} instance. | |
82 | */ | |
83 | private AuthenticationStrategy authenticationStrategy; | |
84 | ||
85 | /*-------------------------------------------- | |
86 | | C O N S T R U C T O R S | | |
87 | ============================================*/ | |
88 | ||
89 | /** | |
90 | * Default no-argument constructor which | |
91 | * {@link #setAuthenticationStrategy(AuthenticationStrategy) enables} an | |
92 | * {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy} by default. | |
93 | */ | |
94 | 44 | public ModularRealmAuthenticator() { |
95 | 44 | this.authenticationStrategy = new AtLeastOneSuccessfulStrategy(); |
96 | 44 | } |
97 | ||
98 | /*-------------------------------------------- | |
99 | | A C C E S S O R S / M O D I F I E R S | | |
100 | ============================================*/ | |
101 | ||
102 | /** | |
103 | * Sets all realms used by this Authenticator, providing PAM (Pluggable Authentication Module) configuration. | |
104 | * | |
105 | * @param realms the realms to consult during authentication attempts. | |
106 | */ | |
107 | public void setRealms(Collection<Realm> realms) { | |
108 | 38 | this.realms = realms; |
109 | 38 | } |
110 | ||
111 | /** | |
112 | * Returns the realm(s) used by this {@code Authenticator} during an authentication attempt. | |
113 | * | |
114 | * @return the realm(s) used by this {@code Authenticator} during an authentication attempt. | |
115 | */ | |
116 | protected Collection<Realm> getRealms() { | |
117 | 62 | return this.realms; |
118 | } | |
119 | ||
120 | /** | |
121 | * Returns the {@code AuthenticationStrategy} utilized by this modular authenticator during a multi-realm | |
122 | * log-in attempt. This object is only used when two or more Realms are configured. | |
123 | * <p/> | |
124 | * Unless overridden by | |
125 | * the {@link #setAuthenticationStrategy(AuthenticationStrategy)} method, the default implementation | |
126 | * is the {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy}. | |
127 | * | |
128 | * @return the {@code AuthenticationStrategy} utilized by this modular authenticator during a log-in attempt. | |
129 | * @since 0.2 | |
130 | */ | |
131 | public AuthenticationStrategy getAuthenticationStrategy() { | |
132 | 4 | return authenticationStrategy; |
133 | } | |
134 | ||
135 | /** | |
136 | * Allows overriding the default {@code AuthenticationStrategy} utilized during multi-realm log-in attempts. | |
137 | * This object is only used when two or more Realms are configured. | |
138 | * | |
139 | * @param authenticationStrategy the strategy implementation to use during log-in attempts. | |
140 | * @since 0.2 | |
141 | */ | |
142 | public void setAuthenticationStrategy(AuthenticationStrategy authenticationStrategy) { | |
143 | 2 | this.authenticationStrategy = authenticationStrategy; |
144 | 2 | } |
145 | ||
146 | /*-------------------------------------------- | |
147 | | M E T H O D S | | |
148 | ||
149 | /** | |
150 | * Used by the internal {@link #doAuthenticate} implementation to ensure that the {@code realms} property | |
151 | * has been set. The default implementation ensures the property is not null and not empty. | |
152 | * | |
153 | * @throws IllegalStateException if the {@code realms} property is configured incorrectly. | |
154 | */ | |
155 | ||
156 | protected void assertRealmsConfigured() throws IllegalStateException { | |
157 | 27 | Collection<Realm> realms = getRealms(); |
158 | 27 | if (CollectionUtils.isEmpty(realms)) { |
159 | 1 | String msg = "Configuration error: No realms have been configured! One or more realms must be " + |
160 | "present to execute an authentication attempt."; | |
161 | 1 | throw new IllegalStateException(msg); |
162 | } | |
163 | 26 | } |
164 | ||
165 | /** | |
166 | * Performs the authentication attempt by interacting with the single configured realm, which is significantly | |
167 | * simpler than performing multi-realm logic. | |
168 | * | |
169 | * @param realm the realm to consult for AuthenticationInfo. | |
170 | * @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials. | |
171 | * @return the AuthenticationInfo associated with the user account corresponding to the specified {@code token} | |
172 | */ | |
173 | protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { | |
174 | 24 | if (!realm.supports(token)) { |
175 | 1 | String msg = "Realm [" + realm + "] does not support authentication token [" + |
176 | token + "]. Please ensure that the appropriate Realm implementation is " + | |
177 | "configured correctly or that the realm accepts AuthenticationTokens of this type."; | |
178 | 1 | throw new UnsupportedTokenException(msg); |
179 | } | |
180 | 23 | AuthenticationInfo info = realm.getAuthenticationInfo(token); |
181 | 19 | if (info == null) { |
182 | 1 | String msg = "Realm [" + realm + "] was unable to find account data for the " + |
183 | "submitted AuthenticationToken [" + token + "]."; | |
184 | 1 | throw new UnknownAccountException(msg); |
185 | } | |
186 | 18 | return info; |
187 | } | |
188 | ||
189 | /** | |
190 | * Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object | |
191 | * as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}. | |
192 | * | |
193 | * @param realms the multiple realms configured on this Authenticator instance. | |
194 | * @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials. | |
195 | * @return an aggregated AuthenticationInfo instance representing account data across all the successfully | |
196 | * consulted realms. | |
197 | */ | |
198 | protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { | |
199 | ||
200 | 2 | AuthenticationStrategy strategy = getAuthenticationStrategy(); |
201 | ||
202 | 2 | AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); |
203 | ||
204 | 2 | if (log.isTraceEnabled()) { |
205 | 2 | log.trace("Iterating through {} realms for PAM authentication", realms.size()); |
206 | } | |
207 | ||
208 | 2 | for (Realm realm : realms) { |
209 | ||
210 | 4 | aggregate = strategy.beforeAttempt(realm, token, aggregate); |
211 | ||
212 | 4 | if (realm.supports(token)) { |
213 | ||
214 | 4 | log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); |
215 | ||
216 | 4 | AuthenticationInfo info = null; |
217 | 4 | Throwable t = null; |
218 | try { | |
219 | 4 | info = realm.getAuthenticationInfo(token); |
220 | 1 | } catch (Throwable throwable) { |
221 | 1 | t = throwable; |
222 | 1 | if (log.isWarnEnabled()) { |
223 | 1 | String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; |
224 | 1 | log.warn(msg, t); |
225 | } | |
226 | 3 | } |
227 | ||
228 | 4 | aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); |
229 | ||
230 | 4 | } else { |
231 | 0 | log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); |
232 | } | |
233 | 4 | } |
234 | ||
235 | 2 | aggregate = strategy.afterAllAttempts(token, aggregate); |
236 | ||
237 | 2 | return aggregate; |
238 | } | |
239 | ||
240 | ||
241 | /** | |
242 | * Attempts to authenticate the given token by iterating over the internal collection of | |
243 | * {@link Realm}s. For each realm, first the {@link Realm#supports(org.apache.shiro.authc.AuthenticationToken)} | |
244 | * method will be called to determine if the realm supports the {@code authenticationToken} method argument. | |
245 | * <p/> | |
246 | * If a realm does support | |
247 | * the token, its {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} | |
248 | * method will be called. If the realm returns a non-null account, the token will be | |
249 | * considered authenticated for that realm and the account data recorded. If the realm returns {@code null}, | |
250 | * the next realm will be consulted. If no realms support the token or all supporting realms return null, | |
251 | * an {@link AuthenticationException} will be thrown to indicate that the user could not be authenticated. | |
252 | * <p/> | |
253 | * After all realms have been consulted, the information from each realm is aggregated into a single | |
254 | * {@link AuthenticationInfo} object and returned. | |
255 | * | |
256 | * @param authenticationToken the token containing the authentication principal and credentials for the | |
257 | * user being authenticated. | |
258 | * @return account information attributed to the authenticated user. | |
259 | * @throws IllegalStateException if no realms have been configured at the time this method is invoked | |
260 | * @throws AuthenticationException if the user could not be authenticated or the user is denied authentication | |
261 | * for the given principal and credentials. | |
262 | */ | |
263 | protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { | |
264 | 27 | assertRealmsConfigured(); |
265 | 26 | Collection<Realm> realms = getRealms(); |
266 | 26 | if (realms.size() == 1) { |
267 | 24 | return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); |
268 | } else { | |
269 | 2 | return doMultiRealmAuthentication(realms, authenticationToken); |
270 | } | |
271 | } | |
272 | ||
273 | /** | |
274 | * First calls <code>super.onLogout(principals)</code> to ensure a logout notification is issued, and for each | |
275 | * wrapped {@code Realm} that implements the {@link LogoutAware LogoutAware} interface, calls | |
276 | * <code>((LogoutAware)realm).onLogout(principals)</code> to allow each realm the opportunity to perform | |
277 | * logout/cleanup operations during an user-logout. | |
278 | * <p/> | |
279 | * Shiro's Realm implementations all implement the {@code LogoutAware} interface by default and can be | |
280 | * overridden for realm-specific logout logic. | |
281 | * | |
282 | * @param principals the application-specific Subject/user identifier. | |
283 | */ | |
284 | public void onLogout(PrincipalCollection principals) { | |
285 | 9 | super.onLogout(principals); |
286 | 9 | Collection<Realm> realms = getRealms(); |
287 | 9 | if (!CollectionUtils.isEmpty(realms)) { |
288 | 9 | for (Realm realm : realms) { |
289 | 9 | if (realm instanceof LogoutAware) { |
290 | 9 | ((LogoutAware) realm).onLogout(principals); |
291 | } | |
292 | 9 | } |
293 | } | |
294 | 9 | } |
295 | } |