共计 6408 个字符,预计需要花费 17 分钟才能阅读完成。
shrio 是一个比拟轻量级的平安框架,次要的作用是在后端承当认证和受权的工作。明天就讲一下 shrio 进行认证的一个过程。
首先先介绍一下在认证过程中的几个要害的对象:
- Subject:主体
拜访零碎的用户,主体能够是用户、程序等,进行认证的都称为主体; - Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具备唯一性,如用户名、手机号、邮箱地址等,一个主体能够有多个身份,然而必须有一个主身份(Primary Principal)。 - credential:凭证信息
是只有主体本人晓得的平安信息,如明码、证书等。
接着咱们就进入认证的具体过程:
首先是从前端的登录表单中接管到用户输出的 token(username + password):
@RequestMapping("/login")
public String login(@RequestBody Map user){Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.get("email").toString(), user.get("password").toString());
try {subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {return "邮箱不存在!";} catch (AuthenticationException e) {return "账号或明码谬误!";}
return "登录胜利!";
}
这里的 usernamePasswordToken(以下简称 token)就是用户名和明码的一个联合对象,而后调用 subject 的 login 办法将 token 传入开始认证过程。
接着会发现 subject 的 login 办法调用的其实是 securityManager 的 login 办法:
Subject subject = securityManager.login(this, token);
再往下看 securityManager 的 login 办法外部:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {info = authenticate(token);
} catch (AuthenticationException ae) {
try {onFailedLogin(token, ae, subject);
} catch (Exception e) {if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an" +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
下面代码的关键在于:
info = authenticate(token);
行将 token 传入 authenticate 办法中失去一个 AuthenticationInfo 类型的认证信息。
以下是 authenticate 办法的具体内容:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {if (token == null) {throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {info = doAuthenticate(token);
if (info == null) {String msg = "No account information found for authentication token [" + token + "] by this" +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate: String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected" +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {notifyFailure(token, ae);
} catch (Throwable t2) {if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?." +
"Please check your AuthenticationListener implementation(s). Logging sending exception" +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
首先就是判断 token 是否为空,不为空再将 token 传入 doAuthenticate 办法中:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {return doMultiRealmAuthentication(realms, authenticationToken);
}
}
这一步是判断是有单个 Reaml 验证还是多个 Reaml 验证,单个就执行 doSingleRealmAuthentication() 办法,多个就执行 doMultiRealmAuthentication() 办法。
个别状况下是单个验证:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {if (!realm.supports(token)) {String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is" +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {String msg = "Realm [" + realm + "] was unable to find account data for the" +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
这一步中首先判断是否反对 Realm,只有反对 Realm 才调用 realm.getAuthenticationInfo(token) 获取 info。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {cacheAuthenticationInfoIfPossible(token, info);
}
} else {log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {assertCredentialsMatch(token, info);
} else {log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
首先查看 Cache 中是否有该 token 的 info,如果有,则间接从 Cache 中去即可。如果是第一次登录,则 Cache 中不会有该 token 的 info,须要调用 doGetAuthenticationInfo(token) 办法获取,并将后果退出到 Cache 中,不便下次应用。而这里调用的 doGetAuthenticationInfo() 办法就是咱们在本人重写的办法,具体的内容是自定义了对拿到的这个 token 的一个解决的过程:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {if (authenticationToken.getPrincipal() == null)
return null;
String email = authenticationToken.getPrincipal().toString();
User user = userService.findByEmail(email);
if (user == null)
return null;
else return new SimpleAuthenticationInfo(email, user.getPassword(), getName());
}
这其中进行了几步判断:首先是判断传入的用户名是否为空,在判断传入的用户名在本地的数据库中是否存在,不存在则返回一个用户名不存在的 Exception。以上两部通过之后生成一个包含传入用户名和明码的 info,留神此时对于用户名的验证曾经实现,接下来进入对明码的验证。
将这一步失去的 info 返回给 getAuthenticationInfo 办法中的
assertCredentialsMatch(token, info);
此时的 info 是正确的用户名和明码的信息,token 是输出的用户名和明码的信息,通过后面步骤的验证过程,用户名此时曾经是真是存在的了,这一步就是验证输出的用户名和明码的对应关系是否正确。
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
}
else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify" +
"credentials during authentication. If you do not wish for credentials to be examined, you" +
"can configure an" + AllowAllCredentialsMatcher.class.getName() + "instance.");
}
}
下面步骤就是验证 token 中的明码的和 info 中的明码是否对应的代码。这一步验证实现之后,整个 shrio 认证的过程就完结了。