注:例子的原作者是 Paul Feuer和John Musser,我在上面加入些注释以便更好的理解Jaas的实现
首先,我们来看一下 JAAS 一个认证操作的实现流程
先看一下这个认证操作会使用的接口如下:
javax.security.auth.callback.CallbackHandler
javax.security.auth.spi.LoginModule
下面看一下这个基于JAAS认证的实现流程:
JaasTest 实现一个登录的请求操作:
1 public class JaasTest {
2
3 public static void main(String[] args) {
4
5 boolean loginSuccess = false;
6 Subject subject = null;
7
8 try {
9 // ConsoleCallbackHandler 实现CallbackHandler接口 处理用户名和密码(这里从键盘输入)
10 ConsoleCallbackHandler cbh = new ConsoleCallbackHandler();
11 // 启动LoginContext, 装载LoginModule
12 //login module配置文件,通过java -Djava.security.auth.login.config=jaas.config指定
13 //其中 example就是module名字
14 LoginContext lc = new LoginContext("Example", cbh);
15
16 try {
17 lc.login(); //进行登录,它会回调 CallbackHandler的handle方法,要求
18 //handle方法把传入的参数Callback[]中找到NameCallback类型,调用NameCallback.setName把
19 //用户名传进去,找到PasswordCallback类型,调用PasswordCallback.setPassword方法把密码传进去
20 //如果登录失败,则把抛LoginException异常
21 loginSuccess = true;
22 //登录成功后,可以返回Subject对象 1.(保存一组(Set)Principal对象),一般保存人员信息
23 //2.(保存一组(Set) Credential Object getPublicCredentials(Class<T> c) 一般保存密码
24 subject = lc.getSubject();
25
26 Iterator it = subject.getPrincipals().iterator();
27 while (it.hasNext())
28 System.out.println("Authenticated: " + it.next().toString());
29
30 it = subject.getPublicCredentials(Properties.class).iterator();
31 while (it.hasNext())
32 ((Properties)it.next()).list(System.out);
33
34 lc.logout(); //注销
35 } catch (LoginException lex) {
36 System.out.println(lex.getClass().getName() + ": " + lex.getMessage());
37 }
38 } catch (Exception ex) {
39 System.out.println(ex.getClass().getName() + ": " + ex.getMessage());
40 }
41
42 System.exit(0);
43 }
44 }
实现步骤如下:
A:
//创建LoginContext实例,把login module 名称和CallbackHandler接口实现
LoginContext lc = new LoginContext("Example", cbh);
B:
lc.login //进行登录操作,此时,会根据通过java -Djava.security.auth.login.config=jaas.config指定
//的配置, 加jaas.config文件 jaas.config写法如下:
Example {
RdbmsLoginModule required debug="true" url="jdbc:mysql://localhost/jaasdb?user=root&password=pass" driver="org.gjt.mm.mysql.Driver";
};
//login module name
Example1 {
//RdbmsLoginModule: module class name
//required 固定
//debug url driver都为参数,可以在LoginModule实现类的 initialize(Subject subject, CallbackHandler callbackHandler,
// Map sharedState, Map options)方法中取得
RdbmsLoginModule required debug="true" url="jdbc:mysql://localhost/jaasdb?user=root&password=pass" driver="org.gjt.mm.mysql.Driver";
};
接下来,LoginContext会去 回调 CallbackHandler的handle(Callback[] callbacks)方法
handle方法把传入的参数Callback[]中找到NameCallback类型,调用NameCallback.setName把
用户名传进去,找到PasswordCallback类型,调用PasswordCallback.setPassword方法把密码传进去
下面来看一下ConsoleCallbackHandler 实现代码:
1 public class ConsoleCallbackHandler implements CallbackHandler {
2
3 /**
4 * <p>Creates a callback handler that prompts and reads from the
5 * command line for answers to authentication questions.
6 * This can be used by JAAS applications to instantiate a
7 * CallbackHandler.
8 */
9 public ConsoleCallbackHandler() {
10 }
11
12 /**
13 * Handles the specified set of callbacks.
14 * This class supports NameCallback and PasswordCallback.
15 *
16 * @param callbacks the callbacks to handle
17 * @throws IOException if an input or output error occurs.
18 * @throws UnsupportedCallbackException if the callback is not an
19 * instance of NameCallback or PasswordCallback
20 */
21 public void handle(Callback[] callbacks)
22 throws java.io.IOException, UnsupportedCallbackException {
23
24 for (int i = 0; i < callbacks.length; i++) {
25 System.out.println(callbacks[i]);
26 if (callbacks[i] instanceof NameCallback) {
27 System.out.print(((NameCallback)callbacks[i]).getPrompt());
28 String user=(new BufferedReader(new InputStreamReader(System.in))).readLine();
29 //设置用户名
30 ((NameCallback)callbacks[i]).setName(user);
31 } else if (callbacks[i] instanceof PasswordCallback) {
32 System.out.print(((PasswordCallback)callbacks[i]).getPrompt());
33 String pass=(new BufferedReader(new InputStreamReader(System.in))).readLine();
34 //设置密码
35 ((PasswordCallback)callbacks[i]).setPassword(pass.toCharArray());
36 } else {
37 throw(new UnsupportedCallbackException(
38 callbacks[i], "Callback class not supported"));
39 }
40 }
41 }
42 }
43
C:
接下来,loginContext会 回调依次调用 LoginModule的 方法, 次序如下:
LoginModule.login
LoginModule.commit
如果commit返回 false,则会调用 LoginModule.abort方法
看一下RdbmsLoginModule类的实现:
1 public class RdbmsLoginModule implements LoginModule {
2
3 // initial state
4 CallbackHandler callbackHandler;
5 Subject subject;
6 Map sharedState;
7 Map options;
8
9 // temporary state
10 Vector tempCredentials;
11 Vector tempPrincipals;
12
13 // the authentication status
14 boolean success;
15
16 // configurable options
17 boolean debug;
18 String url;
19 String driverClass;
20
21 /**
22 * <p>Creates a login module that can authenticate against
23 * a JDBC datasource.
24 */
25 public RdbmsLoginModule() {
26 tempCredentials = new Vector();
27 tempPrincipals = new Vector();
28 success = false;
29 debug = false;
30 }
31
32 /**
33 * Initialize this <code>LoginModule</code>.
34 *
35 * <p>
36 *
37 * @param subject the <code>Subject</code> to be authenticated. <p>
38 *
39 * @param callbackHandler a <code>CallbackHandler</code> for communicating
40 * with the end user (prompting for usernames and
41 * passwords, for example). <p>
42 *
43 * @param sharedState shared <code>LoginModule</code> state. <p>
44 *
45 * @param options options specified in the login
46 * <code>Configuration</code> for this particular
47 * <code>LoginModule</code>.
48 */
49 public void initialize(Subject subject, CallbackHandler callbackHandler,
50 Map sharedState, Map options) {
51
52 // save the initial state
53 this.callbackHandler = callbackHandler;
54 this.subject = subject;
55 this.sharedState = sharedState;
56 this.options = options;
57
58 // initialize any configured options
59 if (options.containsKey("debug"))
60 debug = "true".equalsIgnoreCase((String)options.get("debug"));
61
62 url = (String)options.get("url");
63 driverClass = (String)options.get("driver");
64
65 if (debug) {
66 System.out.println("\t\t[RdbmsLoginModule] initialize");
67 System.out.println("\t\t[RdbmsLoginModule] url: " + url);
68 System.out.println("\t\t[RdbmsLoginModule] driver: " + driverClass);
69 }
70 }
71
72 /**
73 * <p> Verify the password against the relevant JDBC datasource.
74 *
75 * @return true always, since this <code>LoginModule</code>
76 * should not be ignored.
77 *
78 * @exception FailedLoginException if the authentication fails. <p>
79 *
80 * @exception LoginException if this <code>LoginModule</code>
81 * is unable to perform the authentication.
82 */
83 public boolean login() throws LoginException {
84
85 if (debug)
86 System.out.println("\t\t[RdbmsLoginModule] login");
87
88 if (callbackHandler == null)
89 throw new LoginException("Error: no CallbackHandler available " +
90 "to garner authentication information from the user");
91
92 try {
93 // Setup default callback handlers.
94 Callback[] callbacks = new Callback[] {
95 new NameCallback("Username: "),
96 new PasswordCallback("Password: ", false)
97 };
98
99 callbackHandler.handle(callbacks);
100
101 String username = ((NameCallback)callbacks[0]).getName();
102 String password = new String(((PasswordCallback)callbacks[1]).getPassword());
103
104 ((PasswordCallback)callbacks[1]).clearPassword();
105
106 success = rdbmsValidate(username, password);
107
108 callbacks[0] = null;
109 callbacks[1] = null;
110
111 if (!success)
112 throw new LoginException("Authentication failed: Password does not match");
113
114 return(true);
115 } catch (LoginException ex) {
116 throw ex;
117 } catch (Exception ex) {
118 success = false;
119 throw new LoginException(ex.getMessage());
120 }
121 }
122
123 /**
124 * Abstract method to commit the authentication process (phase 2).
125 *
126 * <p> This method is called if the LoginContext's
127 * overall authentication succeeded
128 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
129 * succeeded).
130 *
131 * <p> If this LoginModule's own authentication attempt
132 * succeeded (checked by retrieving the private state saved by the
133 * <code>login</code> method), then this method associates a
134 * <code>RdbmsPrincipal</code>
135 * with the <code>Subject</code> located in the
136 * <code>LoginModule</code>. If this LoginModule's own
137 * authentication attempted failed, then this method removes
138 * any state that was originally saved.
139 *
140 * <p>
141 *
142 * @exception LoginException if the commit fails
143 *
144 * @return true if this LoginModule's own login and commit
145 * attempts succeeded, or false otherwise.
146 */
147 public boolean commit() throws LoginException {
148
149 if (debug)
150 System.out.println("\t\t[RdbmsLoginModule] commit");
151
152 if (success) {
153
154 if (subject.isReadOnly()) {
155 throw new LoginException ("Subject is Readonly");
156 }
157
158 try {
159 Iterator it = tempPrincipals.iterator();
160
161 if (debug) {
162 while (it.hasNext())
163 System.out.println("\t\t[RdbmsLoginModule] Principal: " + it.next().toString());
164 }
165
166 subject.getPrincipals().addAll(tempPrincipals);
167 subject.getPublicCredentials().addAll(tempCredentials);
168
169 tempPrincipals.clear();
170 tempCredentials.clear();
171
172 if(callbackHandler instanceof PassiveCallbackHandler)
173 ((PassiveCallbackHandler)callbackHandler).clearPassword();
174
175 return(true);
176 } catch (Exception ex) {
177 ex.printStackTrace(System.out);
178 throw new LoginException(ex.getMessage());
179 }
180 } else {
181 tempPrincipals.clear();
182 tempCredentials.clear();
183 return(true);
184 }
185 }
186
187 /**
188 * <p> This method is called if the LoginContext's
189 * overall authentication failed.
190 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
191 * did not succeed).
192 *
193 * <p> If this LoginModule's own authentication attempt
194 * succeeded (checked by retrieving the private state saved by the
195 * <code>login</code> and <code>commit</code> methods),
196 * then this method cleans up any state that was originally saved.
197 *
198 * <p>
199 *
200 * @exception LoginException if the abort fails.
201 *
202 * @return false if this LoginModule's own login and/or commit attempts
203 * failed, and true otherwise.
204 */
205 public boolean abort() throws javax.security.auth.login.LoginException {
206
207 if (debug)
208 System.out.println("\t\t[RdbmsLoginModule] abort");
209
210 // Clean out state
211 success = false;
212
213 tempPrincipals.clear();
214 tempCredentials.clear();
215
216 if (callbackHandler instanceof PassiveCallbackHandler)
217 ((PassiveCallbackHandler)callbackHandler).clearPassword();
218
219 logout();
220
221 return(true);
222 }
223
224 /**
225 * Logout a user.
226 *
227 * <p> This method removes the Principals
228 * that were added by the <code>commit</code> method.
229 *
230 * <p>
231 *
232 * @exception LoginException if the logout fails.
233 *
234 * @return true in all cases since this <code>LoginModule</code>
235 * should not be ignored.
236 */
237 public boolean logout() throws javax.security.auth.login.LoginException {
238
239 if (debug)
240 System.out.println("\t\t[RdbmsLoginModule] logout");
241
242 tempPrincipals.clear();
243 tempCredentials.clear();
244
245 if (callbackHandler instanceof PassiveCallbackHandler)
246 ((PassiveCallbackHandler)callbackHandler).clearPassword();
247
248 // remove the principals the login module added
249 Iterator it = subject.getPrincipals(RdbmsPrincipal.class).iterator();
250 while (it.hasNext()) {
251 RdbmsPrincipal p = (RdbmsPrincipal)it.next();
252 if(debug)
253 System.out.println("\t\t[RdbmsLoginModule] removing principal "+p.toString());
254 subject.getPrincipals().remove(p);
255 }
256
257 // remove the credentials the login module added
258 it = subject.getPublicCredentials(RdbmsCredential.class).iterator();
259 while (it.hasNext()) {
260 RdbmsCredential c = (RdbmsCredential)it.next();
261 if(debug)
262 System.out.println("\t\t[RdbmsLoginModule] removing credential "+c.toString());
263 subject.getPrincipals().remove(c);
264 }
265
266 return(true);
267 }
268
269 /**
270 * Validate the given user and password against the JDBC datasource.
271 * <p>
272 *
273 * @param user the username to be authenticated. <p>
274 * @param pass the password to be authenticated. <p>
275 * @exception Exception if the validation fails.
276 */
277 private boolean rdbmsValidate(String user, String pass) throws Exception {
278
279 Connection con;
280 String query = "SELECT * FROM USER_AUTH where userid='" + user + "'";
281 Statement stmt;
282 RdbmsPrincipal p = null;
283 RdbmsCredential c = null;
284 boolean passwordMatch = false;
285
286 try {
287 Class.forName(driverClass);
288 }
289 catch (java.lang.ClassNotFoundException e) {
290 System.err.print("ClassNotFoundException: ");
291 System.err.println(e.getMessage());
292 throw new LoginException("Database driver class not found: " + driverClass);
293 }
294
295 try {
296 if (debug)
297 System.out.println("\t\t[RdbmsLoginModule] Trying to connect");
298
299 con = DriverManager.getConnection(url, "Administrator", "");
300
301 if (debug)
302 System.out.println("\t\t[RdbmsLoginModule] connected!");
303
304 stmt = con.createStatement();
305
306 if (debug)
307 System.out.println("\t\t[RdbmsLoginModule] "+query);
308
309 ResultSet result = stmt.executeQuery(query);
310 String dbPassword = null, dbFname = null, dbLname = null;
311 String updatePerm = null, deletePerm = null;
312 boolean isEqual = false;
313
314 while (result.next()) {
315 if (!result.isFirst())
316 throw new LoginException("Ambiguous user (located more than once): "+user);
317 dbPassword = result.getString(result.findColumn("password"));
318 dbFname = result.getString(result.findColumn("first_name"));
319 dbLname = result.getString(result.findColumn("last_name"));
320 deletePerm = result.getString(result.findColumn("delete_perm"));
321 updatePerm = result.getString(result.findColumn("update_perm"));
322 }
323
324 if (dbPassword == null)
325 throw new LoginException("User " + user + " not found");
326
327 if (debug)
328 System.out.println("\t\t[RdbmsLoginModule] '"+pass + "' equals '" + dbPassword + "'?");
329
330 passwordMatch = pass.equals(dbPassword);
331 if (passwordMatch) {
332 if (debug)
333 System.out.println("\t\t[RdbmsLoginModule] passwords match!");
334
335 c = new RdbmsCredential();
336 c.setProperty("delete_perm", deletePerm);
337 c.setProperty("update_perm", updatePerm);
338 this.tempCredentials.add(c);
339 this.tempPrincipals.add(new RdbmsPrincipal(dbFname + " " + dbLname));
340 } else {
341 if (debug)
342 System.out.println("\t\t[RdbmsLoginModule] passwords do NOT match!");
343 }
344 stmt.close();
345 con.close();
346 }
347 catch (SQLException ex) {
348 System.err.print("SQLException: ");
349 System.err.println(ex.getMessage());
350 throw new LoginException("SQLException: "+ex.getMessage());
351 }
352 return(passwordMatch);
353 }
354 }
355
Good Luck!
Yours Matthew!