目录
1.用户登录认证流程
1.1 生成认证Token
1.2 用户登录认证
1.2.1 SecurityManager login流程解析
1.2.1.1 authenticate方法进行登录认证
1.2.1.1.1 单Realm认证
1.2.1.2 认证通过后创建登录用户对象
1.2.1.2.1 复制SubjectContext
1.2.1.2.2 对subjectContext设置securityManager
1.2.1.2.3 对subjectContext设置session
1.2.1.2.4 对subjectContext设置Principals
1.2.1.2.5 根据subjectContext创建Subject
1.2.1.2.6 保存Subject
1.3 登录认证成功后处理
Shiro作为一款比较流行的登录认证、访问控制安全框架,被广泛应用在程序员社区;Shiro登录验证、访问控制、Session管理等流程内部都是委托给SecurityManager安全管理器来完成的,SecurityManager安全管理器上篇文章已经进行了详细解析,详见:Shiro框架:Shiro SecurityManager安全管理器解析-CSDN博客,在此基础上,本篇文章继续对Shiro关联链路处理流程之一---登录认证流程 进行解析;
想要深入了解Shiro框架整体原理,可移步:
Shiro框架:ShiroFilterFactoryBean过滤器源码解析-CSDN博客、
Shiro框架:Shiro内置过滤器源码解析-CSDN博客
1.用户登录认证流程
在Shiro框架:Shiro内置过滤器源码解析-CSDN博客内置过滤器分析中,我们知道用户执行登录的认证操作是在过滤器FormAuthenticationFilter中执行的,如下:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
登录认证操作是在executeLogin方法中完成的:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
上述源码的执行过程表示为时序图会更直观,时序图如下:
该认证过程主要包含以下几个部分:
- 根据用户名密码等生成认证Token
- 获取当前登录用户Subject
- 调用Subject的login方法进行登录认证
- 其它的登录成功、或登录失败的拦截方法
下面主要对生成认证Token、用户登录认证实现以及登录成功后处理进行详细说明;
1.1 生成认证Token
createToken的实现在FormAuthenticationFilter内,如下:
这里用户名和密码是通过request请求对象获取的:
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
return createToken(username, password, request, response);
}
在父类AuthenticatingFilter中进一步实现如下,这里构造了UsernamePasswordToken类,表示采用用户名密码的认证方式;
protected AuthenticationToken createToken(String username, String password,
ServletRequest request, ServletResponse response) {
boolean rememberMe = isRememberMe(request);
String host = getHost(request);
return createToken(username, password, rememberMe, host);
}
protected AuthenticationToken createToken(String username, String password,
boolean rememberMe, String host) {
return new UsernamePasswordToken(username, password, rememberMe, host);
}
图示UsernamePasswordToken的继承结构:
1.2 用户登录认证
在Subject的login的具体实现如下:
- 实际的login是委托给SecurityManager完成的
- 登录成功后设置当前登录对象为认证成功
下面主要对SecurityManager的login方法进行具体分析;
1.2.1 SecurityManager login流程解析
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;
}
上述登录流程主要包含2部分内容:
- 通过authenticate方法进行登录认证
- 认证通过后创建登录用户对象
下面分别进行展开分析;
1.2.1.1 authenticate方法进行登录认证
具体的authenticate实现内部又委托给了认证器Authenticator来实现,具体的认证器为ModularRealmAuthenticator,其继承结构如下:
authenticate方法具体实现是在父类AbstractAuthenticator中,如下:
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;
}
通过方法doAuthenticate对认证Token进行认证,通过notifySuccess、notifyFailure监听认证通过或失败的事件并调用注册的监听器;
方法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);
}
}
这里首先获取已注册的安全组件Realm,其注册过程是在RealmSecurityManager初始化的过程中完成的(对SecurityManager安全管理器的处理过程感兴趣可以参见:Shiro框架:Shiro SecurityManager安全管理器解析-CSDN博客)
然后根据Realm的个数分别执行单Realm认证或多Realm认证,这里已单Realm认证为例进行说明,多Realm认证类似;
1.2.1.1.1 单Realm认证
通过doSingleRealmAuthentication方法完成单Realm认证,实现如下:
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;
}
这里Reaml接口包含一整套的继承层次实现,如下,这里不过多展开解析;
getAuthenticationInfo是在AuthenticatingRealm中完成的,如下:
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;
}
这里首先获取AuthenticationInfo对象,要么从缓存中获取,要么通过引入的抽象方法doGetAuthenticationInfo获取(交由应用层子类具体实现);
获取到AuthenticationInfo后,将其与用户录入的登录Token进行比对,这部分是在方法assertCredentialsMatch中完成的,具体如下:
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.");
}
}
这里获取了默认的匹配器SimpleCredentialsMatcher,并调用doCredentialsMatch方法进行匹配,匹配实现如下:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}
首先获取Credentials,也即用户密码,然后调用equals方法进行匹配,如下通过字节流的方式进行比对(主要是为了安全考虑,系统处理时采用非明文形式)。
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" +
tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
accountCredentials.getClass().getName() + "]");
}
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " +
"array equals comparison");
}
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return MessageDigest.isEqual(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
至此,用户账号密码匹配完成,匹配完成后,会重新创建用户登录对象,并更新用户状态等,下面具体分析;
1.2.1.2 认证通过后创建登录用户对象
认证通过后创建Subject的实现如下:
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}
@Override
protected SubjectContext createSubjectContext() {
return new DefaultWebSubjectContext();
}
这里创建了DefaultWebSubjectContext,用户Subject创建,其中设置了认证状态、认证Token、已认证信息,然后调用createSubject方法继续进行构造,如下:
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
如上创建过程包含了以下几部分,分别进行分析;
1.2.1.2.1 复制SubjectContext
对subjectContext调用复制构造方法进行复制,subjectContext底层通过backingMap保存上下文信息:
public MapContext(Map<String, Object> map) {
this();
if (!CollectionUtils.isEmpty(map)) {
this.backingMap.putAll(map);
}
}
1.2.1.2.2 对subjectContext设置securityManager
获取securityManager的顺序如下:
- 首先从subjectContext中获取
- 若无,则从当前线程上下文中获取
- 否则将当前securityManager设置到subjectContext中
protected SubjectContext ensureSecurityManager(SubjectContext context) {
if (context.resolveSecurityManager() != null) {
log.trace("Context already contains a SecurityManager instance. Returning.");
return context;
}
log.trace("No SecurityManager found in context. Adding self reference.");
context.setSecurityManager(this);
return context;
}
public SecurityManager resolveSecurityManager() {
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
if (log.isDebugEnabled()) {
log.debug("No SecurityManager available in subject context map. " +
"Falling back to SecurityUtils.getSecurityManager() lookup.");
}
try {
securityManager = SecurityUtils.getSecurityManager();
} catch (UnavailableSecurityManagerException e) {
if (log.isDebugEnabled()) {
log.debug("No SecurityManager available via SecurityUtils. Heuristics exhausted.", e);
}
}
}
return securityManager;
}
1.2.1.2.3 对subjectContext设置session
解析session方法如下,其中session的解析顺序为:
- 首先从subjectContext获取session,判断是否已设置session(见源码2)
- 否则,通过subjectContext中保存的Subject对象获取关联的session(见源码2)
- 其次,通过sessionManager Session管理器获取(见源码3)
- Web服务中,具体的Session管理器为ServletContainerSessionManager,尝试从request请求中获取Servlet管理的Session;(见源码4)
源码1:
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
log.debug("Context already contains a session. Returning.");
return context;
}
try {
//Context couldn't resolve it directly, let's see if we can since we have direct access to
//the session manager:
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +
"(session-less) Subject instance.", e);
}
return context;
}
源码2:
public Session resolveSession() {
Session session = getSession();
if (session == null) {
//try the Subject if it exists:
Subject existingSubject = getSubject();
if (existingSubject != null) {
session = existingSubject.getSession(false);
}
}
return session;
}
源码3:
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
SessionKey key = getSessionKey(context);
if (key != null) {
return getSession(key);
}
return null;
}
public Session getSession(SessionKey key) throws SessionException {
return this.sessionManager.getSession(key);
}
源码4: ServletContainerSessionManager获取Session
public Session getSession(SessionKey key) throws SessionException {
if (!WebUtils.isHttp(key)) {
String msg = "SessionKey must be an HTTP compatible implementation.";
throw new IllegalArgumentException(msg);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
Session session = null;
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
session = createSession(httpSession, request.getRemoteHost());
}
return session;
}
1.2.1.2.4 对subjectContext设置Principals
resolvePrincipals方法实现如下,这里获取Principals的顺序为:
- 先从SubjectContext中直接获取Principals(见源码2)
- 否则,通过SubjectContext中已认证的AuthenticationInfo获取(见源码2)
- 其次,通过SubjectContext中的Subject获取(见源码2)
- 再次,通过SubjectContext中的Session获取(见源码2)
- 最后,通过RememberMeManager中的Cookie中获取
源码1:
protected SubjectContext resolvePrincipals(SubjectContext context) {
PrincipalCollection principals = context.resolvePrincipals();
if (isEmpty(principals)) {
log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");
principals = getRememberedIdentity(context);
if (!isEmpty(principals)) {
log.debug("Found remembered PrincipalCollection. Adding to the context to be used " +
"for subject construction by the SubjectFactory.");
context.setPrincipals(principals);
} else {
log.trace("No remembered identity found. Returning original context.");
}
}
return context;
}
源码2:从SubjectContext获取Principals
public PrincipalCollection resolvePrincipals() {
PrincipalCollection principals = getPrincipals();
if (isEmpty(principals)) {
//check to see if they were just authenticated:
AuthenticationInfo info = getAuthenticationInfo();
if (info != null) {
principals = info.getPrincipals();
}
}
if (isEmpty(principals)) {
Subject subject = getSubject();
if (subject != null) {
principals = subject.getPrincipals();
}
}
if (isEmpty(principals)) {
//try the session:
Session session = resolveSession();
if (session != null) {
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
}
}
return principals;
}
1.2.1.2.5 根据subjectContext创建Subject
如下,通过createSubject创建了WebDelegatingSubject对象:
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}
1.2.1.2.6 保存Subject
Subject保存是通过DefaultSubjectDAO完成的,
如下,通过saveToSession方法保存Principals和认证状态到Session中;
protected void saveToSession(Subject subject) {
//performs merge logic, only updating the Subject's session if it does not match the current state:
mergePrincipals(subject);
mergeAuthenticationState(subject);
}
这里在保存Principals的过程中,如果Principals不为空,且非Servlet Session的条件下,这里会调用subject.getSession()方法创建shiro管理的native Session对象:
subject.getSession()方法实现如下:
public Session getSession() {
return getSession(true);
}
public Session getSession(boolean create) {
if (log.isTraceEnabled()) {
log.trace("attempting to get session; create = " + create +
"; session is null = " + (this.session == null) +
"; session has id = " + (this.session != null && session.getId() != null));
}
if (this.session == null && create) {
//added in 1.2:
if (!isSessionCreationEnabled()) {
String msg = "Session creation has been disabled for the current subject. This exception indicates " +
"that there is either a programming error (using a session when it should never be " +
"used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
"for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " +
"for more.";
throw new DisabledSessionException(msg);
}
log.trace("Starting session for host {}", getHost());
SessionContext sessionContext = createSessionContext();
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
如上,通过securityManager的start方法创建Session对象,start具体实现为 :
public Session start(SessionContext context) throws AuthorizationException {
return this.sessionManager.start(context);
}
这里又委托给了Session管理器完成session创建,这里的Session管理器实现为AbstractNativeSessionManager,start方法实现如下:
public Session start(SessionContext context) {
Session session = createSession(context);
applyGlobalSessionTimeout(session);
onStart(session, context);
notifyStart(session);
//Don't expose the EIS-tier Session object to the client-tier:
return createExposedSession(session, context);
}
上述主要完成了以下几部分功能:
1. 通过createSession方法创建SimpleSession对象,其中包括了Session创建、Session保存到sessionDAO中(比如保存到内存中的子类 MemorySessionDAO ,或者保存在数据库中的子类EnterpriseCacheSessionDAO等),SessionId生成等操作;
图示sessionDAO的整体继承结构:
2. Session全局超时时间配置(默认超时时间为30min)
3. onStart将SessionId存储到Cookie中,如下:
private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
}
Cookie template = getSessionIdCookie();
Cookie cookie = new SimpleCookie(template);
String idString = currentId.toString();
cookie.setValue(idString);
cookie.saveTo(request, response);
log.trace("Set session ID cookie for session with id {}", idString);
}
1.3 登录认证成功后处理
用户登录认证成功后,调用方法onSuccessfulLogin:
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
rememberMeSuccessfulLogin(token, info, subject);
}
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onSuccessfulLogin(subject, token, info);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onSuccessfulLogin. RememberMe services will not be " +
"performed for account [" + info + "].";
log.warn(msg, e);
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("This " + getClass().getName() + " instance does not have a " +
"[" + RememberMeManager.class.getName() + "] instance configured. RememberMe services " +
"will not be performed for account [" + info + "].");
}
}
}
如上,如果已设置了RememberMe管理器RememberMeManager,会调用其onSuccessfulLogin方法:
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);
//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}
这里,判断如果认证Token开启了RememberMe,会调用rememberIdentity方法执行RememberMe逻辑;
如下,获取用户的账户信息Principals,并保存到Cookie当中:
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
至此,用户登录认证过程完成了生成认证Token、通过Realm从应用中获取用户认证信息、用户认证Token匹配,以及认证成功后创建用户Subject对象、保存Subject到Session、SessionId保存到Cookie、用户账号Principals保存Cookie等操作。