spring集成shiro登陆流程(上)
Posted qiaozhuangshi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring集成shiro登陆流程(上)相关的知识,希望对你有一定的参考价值。
上一篇已经分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪儿呢?
我们看到它的直接父类AbstractShrioFilter继承了OncePerRequestFilter类,该类是shiro内置的大部分filter的父类(抽像公共部分),在该类中定义了doFilter方法
OncePerRequestFilte
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//当前过滤器的名字 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
//如果该过滤器执行过,那么将不执行同一个名字的过滤器 直接执行过滤链中的下一个过滤器 if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { filterChain.doFilter(request, response);
} else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
//如果当前过滤器设置了enabled属性为false,则不执行,直接执行过滤链中的下一个过滤器 filterChain.doFilter(request, response); } else { //标志当前过滤器已经执行过 request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
//1、 核心方法 doFilterInternal(request, response, filterChain); } finally { //过滤链执行完毕后,清空request中的过滤链执行记录 request.removeAttribute(alreadyFilteredAttributeName); } } }
由于我们是通过SpringShiroFilter拦截进来的那么会调用AbstractShrioFilter中的doFilterInternal
AbstractShrioFilter
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { //封装容器的request和response为shiro自己的 其中在request中标识了当前不为servlet容器的session (在创建session时会用到servlet容器调用getSession()时 ) final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); //2、 创建subject(可以看出每次请求都会创建一个Subject对象) final Subject subject = createSubject(request, response); //执行过滤链 subject.execute(new Callable() { public Object call() throws Exception { updateSessionLastAccessTime(request, response); //修改session的最后活动时间 executeChain(request, response, chain); //执行过滤链 return null; } }); } //创建subject对象 protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); } //7、执行过滤链 protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException { //获取当前请求对应的过滤链 FilterChain chain = getExecutionChain(request, response, origChain); chain.doFilter(request, response); }
WebSubject
//3、
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) { //每次都创建subject上下文(subjectContext), 并设置securityManager对象 super(securityManager); //将request和response添加到subject上下文(subjectContext), 即上面创建的对象 setRequest(request); setResponse(response); } //4、创建 public WebSubject buildWebSubject() { Subject subject = super.buildSubject(); return (WebSubject) subject; }
Subject
//5、调用DefaultSecurityManager的createSubject方法
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext); }
DefaultSecurityManager
// 6
public Subject createSubject(SubjectContext subjectContext) { //web的subjectContext时,会重新创建一个新的,其他的(ini等),只是copy SubjectContext context = copy(subjectContext); //验证是否subject上下文中有securityMangary对象,如果没有创建一个 context = ensureSecurityManager(context); //将session放入subjectContext 该session会从cookie或者rul上带的JSESSIONID(默认) 注意:第一次访问项目来到这儿没有session context = resolveSession(context); //校验用户登陆信息, 并放入context, 如果subject,session,和授权认证AuthenticationInfo中都没有,将会从rememberMeManager(cookie)中获取
//注意:第一次访问项目来到这儿没有这些信息 context = resolvePrincipals(context); //创建一个WebDelegatingSubject对象 Subject subject = doCreateSubject(context); //保存当前认证的用户信息(一般是用户名)在session里,并标记当前用户已经被认证过 为了下次remember使用 save(subject); return subject; }
// 从context中获取session protected SubjectContext resolveSession(SubjectContext context) { Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } return context; } protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { //调用的下面子类DefaultWebSecurityManager的方法 SessionKey key = getSessionKey(context); if (key != null) { //调用 SessionsSecurityManager#getSession return getSession(key); } return null; }
//登陆 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { onFailedLogin(token, ae, subject); } Subject loggedIn = createSubject(token, info, subject); //登陆成功后 根据配置的"记住我" 保存认证信息 onSuccessfulLogin(token, info, loggedIn); return loggedIn; } protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { rememberMeSuccessfulLogin(token, info, subject); } protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { //获取 rememberMeManager管理器 RememberMeManager rmm = getRememberMeManager(); rmm.onSuccessfulLogin(subject, token, info); }
DefaultWebSecurityManager
//创建sessionKey
@Override protected SessionKey getSessionKey(SubjectContext context) { //从context中获取sessonId和request,response if (WebUtils.isWeb(context)) { Serializable sessionId = context.getSessionId(); ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); return new WebSessionKey(sessionId, request, response); } else { ... } } // 第一次调用时 getSession方法最后会调用到这儿来 返回null protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); if (sessionId == null) { return null; } Session s = retrieveSessionFromDataSource(sessionId); if (s == null) { //session ID was provided, meaning one is expected to be found, but we couldn‘t find one: String msg = "Could not find session with ID [" + sessionId + "]"; throw new UnknownSessionException(msg); } return s; }
现在开始执行请求路径对应的过滤器
由于过滤链中的过滤器也是OncePerRequestFilte的子类,继续走OncePerRequestFilte#doFilter方法 然后会调用第一步doFilterInternal方法
我们自定义的方法一般也是继承了AdviceFilter过滤器
AdviceFilter
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try {
//8、执行前置方法 boolean continueChain = preHandle(request, response); if (continueChain) { executeChain(request, response, chain); } postHandle(request, response); if (log.isTraceEnabled()) { log.trace("Successfully invoked postHandle method"); } } catch (Exception e) { exception = e; } finally { cleanup(request, response, exception); } }
AccessControlFilter
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { //9、是登陆的rul或者已经认证过 否则重定向到登陆页面 return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
UserFilter
(这里以user过滤器为例,如果没有认证过,直接重定向到登陆url)
该过滤器重写了这两个方法
//10、判断是否认证过
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断当前请求的路径是否为当前过滤器配置的登陆路径(登陆不需要任何权限,返回true) if (isLoginRequest(request, response)) { return true; } else {
//判断是否已经认证过 Subject subject = getSubject(request, response); return subject.getPrincipal() != null; } } //11、返回false protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//重定向到登陆rul saveRequestAndRedirectToLogin(request, response); return false; }
//12、 protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { //调用webUtils的方法,将当前请求失败的的信息保存起来(便于下次认证成功后直接重定向到该路径) saveRequest(request); //重定向的时候会生成session(shrio的) redirectToLogin(request, response); }
WebUtils
//保存
public static void saveRequest(ServletRequest request) { Subject subject = SecurityUtils.getSubject(); //13、这里会创建一个 StoppingAwareProxiedSession AbstractNativeSessionManager#start是创建simpleSession并调用session监听器
//会将sessinID存在cookie中和sessionDao中(默认时ehcache缓存,可以自己实现redis等)(在DefaultSessionManager#create(Session session)方法中) Session session = subject.getSession(); HttpServletRequest httpRequest = toHttp(request); //将当前目标路径的请求信息保存起来 SavedRequest savedRequest = new SavedRequest(httpRequest); //存到session中,跳到登陆页面后登陆成功后会重定向到此次失败的路径 session.setAttribute(SAVED_REQUEST_KEY, savedRequest); } //重定向到savedRequet保存的路径 如果是直接访问的登陆url,则直接重定向到当前过滤器配置的登陆成功url //成功后的重定向可是不生成session的 public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl) throws IOException { String successUrl = null; boolean contextRelative = true; //从session中获取,并清空上一次失败保存的信息 SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); //上一次请求失败的保存的对象 而且是get请求(这里一般是直接浏览器输入的url) 如果是post请求过来的(一般是表单),直接返回目标路径 if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) { successUrl = savedRequest.getRequestUrl(); contextRelative = false; }
//第一次请求时,successUrl为null, 登陆成功后,有值(上一次失败的url) if (successUrl == null) { successUrl = fallbackUrl; }
//15、发出重定向 WebUtils.issueRedirect(request, response, successUrl, null, contextRelative); } public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException { issueRedirect(request, response, url, queryParams, contextRelative, true); } // 会把sessionID写在rul上, 下面的内容就不带大家看了 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException { RedirectView view = new RedirectView(url, contextRelative, http10Compatible); view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response)); }
SavedRequest
// SavedRequest的构造方法 public SavedRequest(HttpServletRequest request) {
//当前请求的方式(get|post...) this.method = request.getMethod();
//当前请求的参数 this.queryString = request.getQueryString();
//当前请求失败的路径 this.requestURI = request.getRequestURI(); }
RedirectView (拼接重定向的参数请求头、url加sessionID,url编码等操作都由这儿进入)
public final void renderMergedOutputModel( Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { // Prepare name URL. StringBuilder targetUrl = new StringBuilder(); if (this.contextRelative && getUrl().startsWith("/")) { targetUrl.append(request.getContextPath()); } targetUrl.append(getUrl());
//拼接请求参数 appendQueryProperties(targetUrl, model, this.encodingScheme); sendRedirect(request, response, targetUrl.toString(), this.http10Compatible); }
AbstractNativeSessionManager
//14、创建session时会调用
public Session start(SessionContext context) { //创建simpleSession Session session = createSession(context); //重置session时间 applyGlobalSessionTimeout(session);
//会将sessionID存到cookie onStart(session, context); //调用session的Listner notifyStart(session); //Don‘t expose the EIS-tier Session object to the client-tier: return createExposedSession(session, context); }
那么此时一个没有授权的请求就执行完毕,现在就来到了我们的登陆界面
登陆使用authc过滤器
FormAuthenticationFilter
和上面的过程一样,会判断是否认证,如果没有会执行onAccessDenied方法
// 这里需要该过滤器的 登陆url 和登陆所在的界面的url一样 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //条件: 配置的该过滤器的登陆路径和请求路径相同 if (isLoginRequest(request, response)) { //1、HttpServletRequest 2、post请求 if (isLoginSubmission(request, response)) { return executeLogin(request, response); } else { //登陆页面的url 请求方式为get return true; } } else { //如果一个请求路径配置的authc过滤器,然后没有登陆直接调用,会走到这里 //重定向到登陆页面 会创建一个StoppingAwareProxiedSession类型的session 并把sessionId放在登陆页面的url上 saveRequestAndRedirectToLogin(request, response); return false; } }
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { //调用 new UsernamePasswordToken(username, password, rememberMe, host); AuthenticationToken token = createToken(request, response); try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } //登陆成功后 重定向到上一次重定向过来的路径或者当前过滤器的登陆路径 //可以重写该方法登陆后直接跳到当前过滤器配置的url 而不是上一次失败的url protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { //调用父类AuthenticationFilter的issueSuccessRedirect方法 issueSuccessRedirect(request, response); //重定向后,阻止过滤连调用 return false; }
AuthenticationFilter
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception { //当前过滤器配置的登陆url WebUtils.redirectToSavedRequest(request, response, getSuccessUrl()); }
DelegatingSubject
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); //委托给securiManager登陆 Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //认证信息 principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } this.principals = principals; //标记已经登陆过 this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } //获取登陆时的session Session session = subject.getSession(false); if (session != null) { //执行new StoppingAwareProxiedSession(session, this); 登陆后的session封装成StoppingAwareProxiedSession代理对象 this.session = decorate(session); } else { this.session = null; } }
AbstractRememberMeManager 处理 remeberme
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { //清空之前的认证信息 forgetIdentity(subject); //如果是rememberMe类型的token if (isRememberMe(token)) { //记录 rememberIdentity(subject, token, info); } } public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { //从认证后的信息中获取 PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); } // 加密处理 protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) { byte[] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); } //使用CipherService类进行处理 protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; } // 返回加密后的认证信息 protected byte[] encrypt(byte[] serialized) { byte[] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null) { // getEncryptionCipherKey() 获取的是rememberMe cookie加密和解密的密钥 ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; } //加密 protected byte[] encrypt(byte[] serialized) { byte[] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
CookieRememberMeManager
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); //base 64 encode it and store as a cookie: String base64 = Base64.encodeToString(serialized); //rememberMe的cookie模板 key为自定义的名字 我这儿是rememberMe Cookie template = getCookie(); Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); }
下面是remember的配置
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/><!-- 30天 --> </bean> <!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- rememberMe cookie加密和解密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)--> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode(‘4AvVhmFLUs0KTA3Kprsdag==‘)}"/> <property name="cookie" ref="rememberMeCookie"/> </bean>
由于篇幅原因,本节详细介绍的是登陆需要验证的请求跳转到登陆界面的源码解析
小结:
1、当第一次请求失败后,会重定向到当前过滤器的登陆界面,并创建一个session,将sessinID存在cookie,重定向的url,还会存放在sessionDao中(默认是ehcache, 可自定义)
2、当请求的路径为不用认证(anon等自定义preHandle返回true的路径),也会由servlet容器调用shiroRequest的getSession方法创建一个session,保存位置同1
以上是关于spring集成shiro登陆流程(上)的主要内容,如果未能解决你的问题,请参考以下文章