发布时间:2023-09-08 13:00
> 有些图片看不清,GitHub里面有:https://github.com/nice01qc/shiro_learn
**shiro介绍(个人理解)**
shiro 就是管理用户权限的一个框架。例如用户提供用户名和密码,shiro拿到之后,根据这个获取用户有哪些角色,通过角色有哪些权限,就可以知道用户可以访问哪些接口(资源)。第一次写分享,写的不是很好,但是本文提供了一个阅读源码的方式,下面是我阅读过程,和一些小总结,且源码可以运行。
### **项目整体介绍:**
------
整个项目克隆自shiro[官方仓库](https://github.com/apache/shiro),只是在子项目 `sapmles`里面添加了2个小项目,用于debug。
- **shiro_learn**(项目根路径)
- samples
- shiro_cas_service 使用springboot简单模拟cas服务
- shiro_client 源码分析开始的地方,shiro大部分配置已经配置好了。
本文将从三个个方面进行分析:
1. **shiro 配置**
2. **ShiroFilterFactoryBean 加载过程**
3. **完整分析一个shiro cas请求**
### **shiro 配置**
------
![shiro](samples/pictures/shiro.png)
上图便是shiro所有功能展示图,从上图可以看出主要分为2大块,
一块是`Security Manager` ,这个模块及授权验证于一身(对应类为:`SecurityManager` 的子类),并通过Subject接口对外提供服务,例如登录、验权等等使用subject就好了(具体使用,后面会详细分析);
另一块就是使用方,就是`SecurityManager` 已经配置好了,第三方应该如何使用呢,本文以`Web MVC` 这个第三方来叙述,`Web MVC` 主要通过 Filter 过滤器拦下所有用户请求,然后分发给相应的 shiro Filter进行相应的处理(例如看看这个请求有没有登录,有没有权限),在Filter中 使用 `SecurityManager` 提供的 `Subject`接口 进行相应的操作。
#### `SecurityManager`配置
![DefaultSecurity](samples/pictures/DefaultSecurity.png)
通过上图可以看出DefaultSecurityManager可以配置的属性有哪些都可以看出来,下面通过表格一一说明:
| 属性 | 是否存在默认配置 | 默认配置类 | 作用说明 | 常用配置 |
| ------------------ | --------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
| subjectFactory | 是 | DefaultSubjectFactory | Subject的具体实现类(对外服务接口,如果是cas服务,建议使用**CasSubjectFactory**覆盖) | 否 |
| subjectDAO | 是 | DefaultSubjectDAO | 主要用于将subject中最新信息保存到session里面 | 否 |
| rememberMeManager | 只存在DefaultWebSecurityManager中 | CookieRememberMeManager | 用于管理rememberMe这个cookie,一般不用 | 否 |
| **sessionManager** | 是 | DefaultSessionManager (DefaultWebSecurityManager 就是DefaultWebSessionManager) | 有关session的操作最终都会委托给他做(他本身还可以配置,见下表) | 是 |
| authorizer | 是 | ModularRealmAuthorizer | 授权策略(多个realm时,可以设置自己的策略) | 否 |
| authenticator | 是 | ModularRealmAuthenticator | 认证策略(多个realm时可以设置自己的策略) | 是 |
| realm | 否 | CasRealm、JdbcRealm ...... | 落实认证和授权操作,需要自己配置(后面会举个例子细讲) | 是 |
| cacheManager | 否 | 使用者最好继承AbstractCacheManager这个抽象类(支持shiro默认周期管理) | 在realm认证和授权的时候会用到(相当于加了一层缓存,cas认证就不需要了,如果是用户名密码,可以使用,增加认证授权速度) | 否(cas就不需要) |
DefaultSecurityManager的sessionManager (DefaultSessionManager)属性配置:
![defaultSessionManager](samples/pictures/defaultSessionManager.png)
| 属性 | 是否存在默认配置 | 默认配置类 | 作用说明 | 常用配置 |
| ----------------- | ---------------- | --------------------------------- | ------------------------------------------------------------ | -------- |
| sessionFactory | 是 | SimpleSessionFactory | 用于创建Session的,一般不配置 | 否 |
| sessionDAO | 是 | MemorySessionDAO | 用于保存Session的接口,一般通过继承**AbstractSessionDAO**类,并使用Redis重新配置一个,将Session保存在redis里面(此抽象类可以配置自己的**SessionIdGenerator** => 用于产生session id的) | 是 |
| casheManager | 否 | | 可以跟实现了CashManagerAware 这个类 的sessionDAO 联合使用,一般就配置sessionDAO就完事 | 否 |
| *sessionIdCookie* | *是* | *new SimpleCookie("JSESSIONID");* | *这个存在于本类的子类DefaultWebSessionManager中,一般都重新配置,这样cookie名字可以改成自己的(一般有关cookie底层的操作都委托给他来做,例如读取此cookie的值,配置cookie ......)* | 是 |
> 注:这些属性通过对象的set方法都可以设置
#### `Web MVC`过滤器配置
DefaultWebSecurityManager 跟DefaultSecurityManager 配置一样,以下说的securityManager 就是指DefaultWebSecurityManager 。
现在DefaultWebSecurityManager 已经配置好了,这个东西应该放在那里呢,肯定得应用到shiro过滤器里面去。
shiro 过滤器相关配置存放在`ShiroFilterFactoryBean`这个类里面,然后使用`DelegatingFilterProxy`将`ShiroFilterFactoryBean`注入到web容器里。
- 如果是springmvc,直接在web.xml内配置这个`DelegatingFilterProxy`就好,过滤器名字就是`ShiroFilterFactoryBean`这个bean的名字;
- 如果是springboot,使用springboot的 `FilterRegistrationBean`进行注册就好(*servlet3.0可以使用ServletContext进行注册,可以注册servlet、filter*......) => **后续会细讲**
`ShiroFilterFactoryBean` ,通过名字就可以看出来,他不是真正的Filter,他使用来配置和管理shirofilter的。
![ShiroFilterFactoryBean](samples/pictures/ShiroFilterFactoryBean.png)
从上图可以看出来,通过FactoryBean 来返回SpringShiroFilter这个对象。`ShiroFilterFactoryBean` 配置说明如下:
| 属性 | 作用说明 |
| ----------------------------------------------- | ------------------------------------------------------------ |
| securityManager | shiro核心 ...... |
| filters (Map
| filterChainDefinitionMap (Map
| loginUrl | 登录URL |
| successUrl | 登录成功后跳转的URL(一般不会使用这个跳转,而使使用第一次访问时保存的url) |
| unauthorizedUrl | 授权不成功跳转的URL |
**以上所有配置例子 放在** `shiro_learn/samples/shiro_clien/src/main/java/com/nice01qc/config/shiro/ShiroCasConfig.java` **这个类里面, 可以对应着看**
### ShiroFilterFactoryBean 加载过程
------
首先我们从项目运行时 ShiroFilterFactoryBean 的加载过程说起(securityManager已经手动配置好了,没什么好说的)。就这样,let's coding .......,所有源码,只指出比较关键的节点,具体细节自己落实
##### ShiroFilterFactoryBean.java
```java
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
// 就是从这个地方开始(如果不知道,请搜索 FactoryBean的作用)
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance(); //直接看这个就好
}
return instance;
}
protected AbstractShiroFilter createInstance() throws Exception {
SecurityManager securityManager = getSecurityManager();
// 封装Filter调用管理,继续深入
FilterChainManager manager = createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//此处创建真正的 总览全局的 shiro filter
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// 获取默认Filter ,取自DefaultFilter枚举类(共12个)
Map
// 将loginUrl、successUrl、unauthorizedUrl填充到符合要求的 filter 内
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
//这是你自己定义的Filter
Map
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter); // 填充一波
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
//'init' argument is false, since Spring-configured filters should be initialized
//in Spring (i.e. 'init-method=blah') or implement InitializingBean:
manager.addFilter(name, filter, false);
}
}
//build up the chains:
Map
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry
// 例如 filterChainDefinitionMap.put("/index", "authc[config1,config2]");
String url = entry.getKey(); // url 就是 "/index"
String chainDefinition = entry.getValue(); // "authc[config1,config2]"
// 会将 chainDefinition的“[config1,config2]” 解析后封装在 authc对应的filter内部,后续会用到
manager.createChain(url, chainDefinition);
}
}
return manager;
}
// 填充那三个值的地方
private void applyGlobalPropertiesIfNecessary(Filter filter) {
applyLoginUrlIfNecessary(filter);
applySuccessUrlIfNecessary(filter);
applyUnauthorizedUrlIfNecessary(filter);
}
private void applyLoginUrlIfNecessary(Filter filter) {
String loginUrl = getLoginUrl();
if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
AccessControlFilter acFilter = (AccessControlFilter) filter;
String existingLoginUrl = acFilter.getLoginUrl();
if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
acFilter.setLoginUrl(loginUrl);
}
}
}
}
```
以上就是ShiroFilterFactoryBean类初始化过程,后续`DelegatingFilterProxy`通过getBean("ShiroFilterFactoryBean的bean name") 来获取这个bean,并将请求委托给他。
*在继续深入之前,先介绍下shiro filter的特色,就是你想实现不同功能的filter,通过继承shiro自带的Filter就好,然后在这基础之上再做修改。*
![AccessControlFilter](samples/pictures/AccessControlFilter.png)
以上每一个抽象类都有不同的作用,分工明确,下面一个一个来分析(上面的loginUrl在上面初始化的时候就注入进去了):=> 这个很关键
##### AbstractFilter.java
```java
// 实现了Filter的init接口,并对外暴露了onFilterConfigSet 接口
public abstract class AbstractFilter extends ServletContextSupport implements Filter {
// 实现了Filter的init接口,并对外暴露了onFilterConfigSet 接口,这个接口在AbstractShiroFilter中覆盖了这个方法,其中AbstractShiroFilter 是 SpringShiroFilter的父类喔,SpringShiroFilter的父类喔,SpringShiroFilter的父类喔
public final void init(FilterConfig filterConfig) throws ServletException {
setFilterConfig(filterConfig);
try {
onFilterConfigSet();
} catch (Exception e) {
}
}
public void destroy() {
}
}
```
##### NameableFilter.java
```java
public abstract class NameableFilter extends AbstractFilter implements Nameable {
// 设置filter 名字用的
public void setName(String name) {
this.name = name;
}
}
```
##### OncePerRequestFilter.java
```java
public abstract class OncePerRequestFilter extends NameableFilter {
// 保证每次请求只访问一次
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 查看是否已经访问过一次
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
// 如果访问了一次,则跳过这个filter,继续下一个
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request, response);
} else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
filterChain.doFilter(request, response);
} else {
// 没有访问,现在标记
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 执行本filter 内容
doFilterInternal(request, response, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
// 子类需要实现的接口,不需要实现doFilter这个方法,通过实现这个方法,可以干更多事
protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException;
}
```
##### AdviceFilter.java
```java
public abstract class AdviceFilter extends OncePerRequestFilter {
// 实现了OncePerRequestFilter这个方法,在这个方法有点像aop的风格
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// 在执行之前,先执行 preHandle 方法
boolean continueChain = preHandle(request, response);
// 如果 preHandle 没通过,将不再继续往下执行
if (continueChain) {
executeChain(request, response, chain);
}
// 执行之后再执行的方法
postHandle(request, response);
} catch (Exception e) {
exception = e;
} finally {
cleanup(request, response, exception);
}
}
// 将这个方法暴露出去
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
return true;
}
// 将这个方法也暴露出去
protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
}
}
```
##### PathMatchingFilter.java
```java
// 用于判断请求是否符合本 Filter,只有请求URL 跟本Filter对应的url匹配规则对上
public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
// ShiroFilterFactoryBean 初始化时就填充进去了,
// 里面value就是那个autho[config1,config2] 中这个[config1,config2]数组
protected Map
// 覆盖了父类的preHandle方法,首先判断请求url是否匹配 本Filter的appliedPaths
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
// 如果Filter本身没有匹配url,返回true
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
return true;
}
for (String path : this.appliedPaths.keySet()) {
//(first match 'wins'):
if (pathsMatch(path, request)) {
Object config = this.appliedPaths.get(path);
// 如果匹配上了就执行这个方法,它会进一步交给onPreHandle方法,并对外暴露这个方法
return isFilterChainContinued(request, response, path, config);
}
}
//no path matched, allow the request to go through:
return true;
}
private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
String path, Object pathConfig) throws Exception {
if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
return onPreHandle(request, response, pathConfig);
}
return true;
}
// 对外暴露此接口
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return true;
}
}
```
##### AccessControlFilter.java(如果你要验证授权什么的,这个类还是比较关键的)
```java
// 此接口用于判断请求是否可以通过,不通过就跳登录,符合要求就使用subject进行登录 等等
public abstract class AccessControlFilter extends PathMatchingFilter {
// 覆盖父类PathMatchingFilter 暴露的方法,并在方法内添加了两种方法
// 一个是isAccessAllowed,用于判断是否已经验证过了,例如用户已经登录过了有session
// 另一个是 onAccessDenied 验证失败,失败后干嘛,鬼知道,你看他怎么实现的
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
// 直接暴露给外界,自己看着实现吧,可别把onAccessDenied的活也干了就好了
protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
// 这个老哥,提供了两个方式供外界覆盖,也就是方法 重载,就是看你要不要那个参数
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return onAccessDenied(request, response);
}
// .......
protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
// 判断是不是登录请求
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
// 保存请求并重定向到登录页面
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request, response);
}
protected void saveRequest(ServletRequest request) {
WebUtils.saveRequest(request);
}
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
String loginUrl = getLoginUrl();
WebUtils.issueRedirect(request, response, loginUrl);
}
}
```
##### **以上做个小总结:**
1. 如果你就想写个简单的Filter,请直接实现Filter接口
2. 如果你想给Filter搞个名字,请继承NameableFilter抽象类
3. 如果你想保证你的filter只被调用了一次,请继承OncePerRequestFilter抽象类
4. 如果你想你的filter在dofilter 方法前后(类似这个方法的aop),请继承AdviceFilter(看着这个Advice是不是特别熟悉-----spring aop也有advice这个概念)
5. 如果你想写个验证和授权的Filter,请继续往下看,因为有两种实现了AccessControlFilter的类(轮子已经建好,上车吧)
#### ***一种是 认证类的(Authenticate):***
AuthenticationFilter.java (一般以ion结尾的单词类,一般提供很基础的服务)
```java
public abstract class AuthenticationFilter extends AccessControlFilter {
// 提供基础的验证
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated() && subject.getPrincipal() != null;
}
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
// 通过验证后,会被重定向到自己设定好的url,这个可以改
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}
}
```
AuthenticatingFilter.java(真正干活的,以后认证什么的继承他就好了,稍微修改就好了)
```java
public abstract class AuthenticatingFilter extends AuthenticationFilter {
// 覆盖了,父类的方法,并在这个方法内部添加了vip功能(可以使用isPermissive走vip通道)
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
// 这是vip模板,仅仅是vip,普通乘客就别走这个了
protected boolean isPermissive(Object mappedValue) {
if(mappedValue != null) {
String[] values = (String[]) mappedValue;
return Arrays.binarySearch(values, PERMISSIVE) >= 0;
}
return false;
}
// 这个方法一般提供给onAccessDenied 方法的,就是你没通过认证,应该登录认证一波了
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
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,因为登录必须拿着token去登录,默认提供了两种生成token的方法,就在这个方法下面喔
protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;
}
```
**最后来一个例子: **
##### **CasFilter.java** (来看看cas他干了什么)
```java
public class CasFilter extends AuthenticatingFilter {
// 直接覆盖父类这个方法,意思就是,你竟然遇到了我,那你就是没有认证(需要被安排下)
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
// 妥妥的实现这个方法,带我去登录吧,我准备好了
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = httpRequest.getParameter(TICKET_PARAMETER);
return new CasToken(ticket);
}
// 去吧皮卡丘,送你去登录
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 所以casFilter 直接执行这一步,前面那些方法都没起作用
return executeLogin(request, response);
}
}
```
*授权类就不说了(同上)*,下面是上面Filter的一些默认实现(DefaultFilter枚举类中可以看到)(是不是很眼熟)
| filter名字 | 对应的Filter |
| ----------------- | ------------------------------------ |
| anon | AnonymousFilter.class |
| authc | FormAuthenticationFilter.class |
| authcBasic | BasicHttpAuthenticationFilter.class |
| authcBearer | BearerHttpAuthenticationFilter.class |
| logout | LogoutFilter.class |
| noSessionCreation | NoSessionCreationFilter.class |
| perms | PermissionsAuthorizationFilter.class |
| port | PortFilter.class |
| rest | HttpMethodPermissionFilter.class |
| roles | RolesAuthorizationFilter.class |
| ssl | SslFilter.class |
| user | UserFilter.class |
通过以上Filter的了解,你是否已经了解shiro系Filter,如果不理解,请再看一遍(这是死循环判断,除非已经看懂,否则不会break;)容器初始化讲完了,是时候来个请求了
### **完整分析一个shiro 请求**
------
那就从请求被Filter拦截那个地方说起吧,到底被谁拦截了,是DelegatingFilterProxy这个类,好了,就在这个类的doFilter方法上打个断点,let's begin ......
##### DelegatingFilterProxy.java
```java
public class DelegatingFilterProxy extends GenericFilterBean {
// 这个方法是在Filter init时调用的,在启动服务过程就会调用,他会初始化delegate,而这个delegate就是ShiroFilterFactoryBean
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
// 什么是targetBeanName 见 DelegatingFilterProxy构造函数
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac); // 直接看这个
}
}
}
}
// 看这个就好了
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
// 直接通过ApplicationContex直接getBean了
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
// targetBeanName的由来
public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) {
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
// doFilter 在这里,来吧!!! 在这里,来吧!!! 在这里,来吧!!!
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
// 直接去这个方法看吧
invokeDelegate(delegateToUse, request, response, filterChain);
}
// 啥也不干,直接就抛给了ShiroFilterFactoryBean的SpringShiroFilter这个内部类
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
}
```
##### SpringShiroFilter
ShiroFilterFactoryBean$SpringShiroFilter的父类OncePerRequestFilter.java(以下便是SpringShiroFilter使命)
```java
public abstract class OncePerRequestFilter extends NameableFilter {
// 跳到这里来了
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request, response);
} else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
filterChain.doFilter(request, response);
} else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 直接看看覆盖了这个方法的类吧
doFilterInternal(request, response, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
}
```
##### SpringShiroFilter的父类AbstractShiroFilter.java
```java
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
// 此处是真正的实现
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)throws ServletException, IOException {
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
// 在此处创建了 subject喔,记住了喔!!!
final Subject subject = createSubject(request, response);
//noinspection unchecked
// subject创建好,并将下面两个方法封装在Callable()里面再执行,
// 在执行这个call之前,先将subject绑定到当前线程,执行完后,清理当前线程的绑定
// 为什么非要搞个Callable,直接在 这两个方法前后放两个方法就好了,可能是因为这样扩展性更强
// 以后在外面再封装一层也方便,说不定还可以搞异步???
subject.execute(new Callable() {
public Object call() throws Exception {
// 更新session时间
updateSessionLastAccessTime(request, response);
//************************************************
// 执行shiro过滤链,(先讲subject创建过程吧,后续再讲)
//************************************************
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
}
}
//*****************************************
// 暂时记这个创建过程为subject创建过程
//*****************************************
// subjcet 创建过程交给了父类Subject$Builder了,并送了他一个securityManager
protected WebSubject createSubject(ServletRequest request, ServletResponse response){
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
}
//==============================================================>
// 简单展示,具体自己debug看看
public class SubjectCallable
public V call() throws Exception {
try {
// 绑定到当前线程
threadState.bind();
// 执行自己实现的那个call方法
return doCall(this.callable);
} finally {
// 清除数据
threadState.restore();
}
}
}
```
#### **subject创建过程**
##### Subject$Builder(Subject内部静态类Build)
```java
public interface Subject {
public static class Builder {
// 从这儿可以看出,最终委托给了SecurityManager来干这个
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
}
}
```
##### DefaultSecurityManager.java(以下讨论的都本类和本类的父类体系内喔)
```java
public class DefaultSecurityManager extends SessionsSecurityManager {
// 在subjcetContext基础上重新new一个喔,不影响前面的subjectContext
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
// 就是一个Map,里面存储了很多验证过程的东西,例如是否认证,是否是remember等等,
// 在DefaultSubjectContext类里面可以看到,有:securityManager,sessionId,authenticationToken,authenticationInfo,subject,principals,session,authenticated,host,sessionCreationEnabled,principalsSessionKey,authenticatedSessionKey
// 就是一个临时状态和工具集合地,如果是DefaultWebSecurityManager就创建一个
// DefaultWebSubjectContext实例,实际上本文讨论的就是web
SubjectContext context = copy(subjectContext);
//确保SecurityManager 已经放到context里面去了
context = ensureSecurityManager(context);
// 这个最关键,也是最复杂一个,也不复杂,就深入了几个类,这也是下面要讲的重点
// step0.........
context = resolveSession(context);
// 这个嘛,等会儿说
context = resolvePrincipals(context);
// 前戏已经准备好了,改开始创建Subject了
Subject subject = doCreateSubject(context);
// 这个会把当前Subject中最新的信息同步到session里面,还有其他功能,后续可以深入看看
save(subject);
return subject;
}
// step1,去解决session
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
return context;
}
try {
// 直接看resolveContextSession
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
}
return context;
}
// step2,开始了
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
// 先看看context里面有没有,有的话就不用继续找了,省时间,没有的话,就将request和response包装到SessionKey里面
SessionKey key = getSessionKey(context);
if (key != null) {
// 现在真正开始了,但这事得分工,直接交给专门管理session的父类 SessionsSecurityManager(代码里面的啃老族,把活全交给父类干,很正常喔)
return getSession(key);
}
return null;
}
}
```
SessionsSecurityManager.java(所有session有关操作,由他管理)
```java
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
// step3. 看到这里,发现SecurityManager整体是不会干活的,就管着整个流程,然后分发出去
public Session getSession(SessionKey key) throws SessionException {
// 这个sessionManger,如果你不覆盖它,默认就是DefaultSessionManager这个类,我们就从这个开始吧
return this.sessionManager.getSession(key);
}
}
```
##### AbstractNativeSessionManager.java(DefaultSessionManager父类)
```java
public abstract class AbstractNativeSessionManager extends AbstractSessionManager implements NativeSessionManager, EventBusAware {
// step4. 别急,渐渐开始了
public Session getSession(SessionKey key) throws SessionException {
Session session = lookupSession(key);
return session != null ? createExposedSession(session, key) : null;
}
// step5. 来了
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
}
// 看到do开头方法,就知道,开始真正干活了
return doGetSession(key);
}
}
```
##### AbstractValidatingSessionManager.java(DefaultSessionManager父类)
```java
public abstract class AbstractValidatingSessionManager extends AbstractNativeSessionManager implements ValidatingSessionManager, Destroyable {
// step6. 开始了
@Override
protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
// 验证session的有效性,一般不启动这个分方法(感觉redis可以设置时间控制有效性,可以不启动验证)
enableSessionValidationIfNecessary();
// 直接看这个吧,这个直接跳到子类DefaultSessionManager中去了,go
Session s = retrieveSession(key);
if (s != null) {
validate(s, key);
}
return s;
}
}
```
##### DefaultSessionManager.java
```java
public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
// step7. 继续
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
// 先取sessionId
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
return null;
}
// 通过sessionId来取session
Session s = retrieveSessionFromDataSource(sessionId);
if (s == null) {
throw new UnknownSessionException(msg);
}
return s;
}
// step8. 开始找sessionId
@Override
public Serializable getSessionId(SessionKey key) {
// 查看key中是否就存储了这个sessionId(shiro到处搞引用缓存,真几把绕)
Serializable id = super.getSessionId(key);
if (id == null && WebUtils.isWeb(key)) {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
// 继续从这儿开始
id = getSessionId(request, response);
}
return id;
}
// 到这里了,继续
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return getReferencedSessionId(request, response);
}
// step9. 这里就找完就结束了,没找到就没找到了,开始回到step7,假定已经找到了,ok,继续
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
// 这个直接从cookie里面读,这个过程建议自己debug进去看看,我感觉挺重要的,也很简单,我就不写了
String id = getSessionIdCookieValue(request, response);
if (id != null) {
// 保存到request里面去
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
} else {
// 如果没找到就从请求参数里面找,这个请求规则是这样的:http:localhost:8001?;ShiroHttpSession.DEFAULT_SESSION_ID_NAME=sessionId(熟称shiro小尾巴,真心不好看)
//try the URI path segment parameters first:
id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
if (id == null) {
//not a URI path segment parameter, try the query parameters:
String name = getSessionIdName();
id = request.getParameter(name);
if (id == null) {
//try lowercase:
// 还没找到,那就从request请求参数里面去找(所以就算浏览器存不了cookie,那只能保存到请求参数里面了)
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
}
}
// 把刚获取到的结果都放在request里面缓存起来
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
// always set rewrite flag - SHIRO-361
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return id;
}
// step10. 从你自己设定的存储来获取session,如果是redis,就从redis里面获取,就到这个了,剩下的自己看吧
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return sessionDAO.readSession(sessionId);
}
}
```
到这一步,resolveSession(context)这个方法已经完成,只剩下doCreateSubject(context)和save(subject)
```java
public class DefaultSecurityManager extends SessionsSecurityManager {
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext);
context = ensureSecurityManager(context);
context = resolveSession(context);
context = resolvePrincipals(context);
// 来这个很简单
Subject subject = doCreateSubject(context);
save(subject);
return subject;
}
// 从方法就看出来,最终使用专门的subjectFactory来创建Subject,本文都在讲web
// 所以默认是 DefaultWebSubjectFactory这个工厂方法
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
}
```
##### DefaultWebSubjectFactory.java
```java
public class DefaultWebSubjectFactory extends DefaultSubjectFactory {
public Subject createSubject(SubjectContext context) {
boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
return super.createSubject(context);
}
// 从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();
// 还有request和response,是不是subject存了很多,但你却基本上没用过,没事别乱搞事喔
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
// 创建一个真正的Subject
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager);
}
}
```
以上就是Subject创建过程,如果有session就填充进去,没有就不填充,但是Subject必须创建出来。好现在让我们回到AbstractShiroFilter这个类,继续看doFilterInternal这个方法,前戏已经足够充分了,改执行shiro 过滤链了。来,come on ...... 马上要讲完了
##### AbstractShiroFilter.java
```java
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
// 已经完成
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
// 来,come on
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
}
if (t != null) {
throw new ServletException(msg, t);
}
}
// 在这里
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
// 这个作用就是根据请求的URL,
// 从 Map
// filterChainDefinitionMap.put("/login", "casFilter");
// filterChainDefinitionMap.put("/favicon.ico", "anon");
// filterChainDefinitionMap.put("/**/*.html", "anon");
// filterChainDefinitionMap.put("/**", "authc,anon"); 这就有2个喔,也就是chain中有两个filter
FilterChain chain = getExecutionChain(request, response, origChain);
// 这个chain就是ProxiedFilterChain(web下就是这个喔),走,去这个类看看,功能很简单
chain.doFilter(request, response);
}
}
```
##### ProxiedFilterChain.java (多个filter时,执行策略)
```java
public class ProxiedFilterChain implements FilterChain {
// 执行过滤链策略,其实就是把当前chain,当成所有filter的chain,使用本地index变量来确定下一个要执行的filter
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
this.orig.doFilter(request, response);
} else {
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}
```
*到现在shiro已经讲了一大半了,还剩下实际运行一个filter过程,我就拿casFilter来讲吧。*
![shiroCas时序图](samples/pictures/shiroCas时序图.png)
从图中可以看出来整个认证过程,我就直接将casFilter.java,这个是用户拿到了那个token,然后向服务器发起了请求,现在CasFilter.java的doFilter拦截到他了,来是时候做个了断了(下面用的Filter抽象类,很熟悉吧,前面讲过喔):
##### OncePerRequestFilter.java(CasFilter的父类)
```java
public abstract class OncePerRequestFilter extends NameableFilter {
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain){
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request, response);
} else
if (!isEnabled(request, response) || shouldNotFilter(request) ) {
filterChain.doFilter(request, response);
} else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 直接看这个吧,我直接跳到CasFilter的onAccessDenied方法吧,
// why => 前面这块讲得真的很清楚喔
// 不知道的请跳到前面讲 shiro filter那块
doFilterInternal(request, response, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
}
```
##### CasFilter.java
```java
public class CasFilter extends AuthenticatingFilter {
// 前面那些步骤啥也没干,直到这里开始干活了
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 这个方法是父类AuthenticatingFilter的,走去这里
return executeLogin(request, response);
}
}
```
##### AuthenticatingFilter.java
```java
public abstract class AuthenticatingFilter extends AuthenticationFilter {
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 取出请求参数里面的token,并包装成casToken
AuthenticationToken token = createToken(request, response);
try {
// 看到这里清楚了吧,使用SecurityManager提供的接口,开始验证了喔
Subject subject = getSubject(request, response);
// 登录,走起
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
}
```
##### DelegatingSubject.java
```java
public class DelegatingSubject implements Subject {
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
// 直接看这个吧,登录操作肯定交给securityManager了
// step1. 开始的地方
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();
}
if (principals == null || principals.isEmpty()) {
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
}
```
##### DefaultSecurityManager.java
```java
public class DefaultSecurityManager extends SessionsSecurityManager {
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// step2. 为托给了父类 AuthenticatingSecurityManager
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
// 失败后,估计就跳转登录了
onFailedLogin(token, ae, subject);
} catch (Exception e) {
}
throw ae; //propagate
}
// 认证成功,重新封装subject
Subject loggedIn = createSubject(token, info, subject);
// 这个跟rememberMe有关
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
}
```
##### AuthenticatingSecurityManager.java
```java
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
// step3. 继续委托给Authenticator,如果你没配置,默认就是ModularRealmAuthenticator
// 本文还是重新配置了(建议多个realm时必须重新配置这个,
// 除非你的认证策略跟ModularRealmAuthenticator一样)
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
// 直接进入
return this.authenticator.authenticate(token);
}
}
```
##### AbstractAuthenticator.java
```java
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
AuthenticationInfo info;
try {
// step4. do开头说明真的开始了,
// 本文是自己实现的Authenticator(MyModularRealmAuthenticator),去这里
info = doAuthenticate(token);
if (info == null) {
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
ae = new AuthenticationException(msg, t);
}
try {
notifyFailure(token, ae);
} catch (Throwable t2) {
}
throw ae;
}
notifySuccess(token, info);
return info;
}
}
```
##### MyModularRealmAuthenticator.java
```java
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
// 当有多个realm时,应该如何使用,本文策略就是:如果是castoken就让他走casRealm,其他的走单个认真方式
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 所有Realm
Collection
HashMap
for (Realm realm : realms) {
realmHashMap.put(realm.getName(), realm);
}
if (authenticationToken instanceof CasToken) {
// step5. 直接进入这个方法吧
return doSingleRealmAuthentication(realmHashMap.get("casRealm"), authenticationToken);
} else {
return doSingleRealmAuthentication(realmHashMap.get("tokenRealm"), authenticationToken);
}
}
}
```
##### ModularRealmAuthenticator.java
```java
public class ModularRealmAuthenticator extends AbstractAuthenticator {
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
throw new UnsupportedTokenException(msg);
}
// step6. 直接进入相应realm了,本文是CasRealm,走去casrealm看看
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
throw new UnknownAccountException(msg);
}
return info;
}
}
```
##### AuthenticatingRealm.java(CasRealm父类)
```java
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 先查看这个realm有没有配置缓存,有的话直接从缓存里面取
// 如果你配置CacheManager,casRealm1.setAuthorizationCachingEnabled(true),则会使用缓存喔,这个在用户名密码登录,在这里加一个缓存,可以加快认证速度,cas则不需要(不是不需要,是不能用)
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
// step7. 来这里吧,一般自己写个realm,就覆盖do开头的方法(因为覆盖就是为了干活喔)
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
}
if (info != null) {
assertCredentialsMatch(token, info);
}
return info;
}
}
```
##### CasRealm.java
```java
public class CasRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
String ticket = (String)casToken.getCredentials();
if (!StringUtils.hasText(ticket)) {
return null;
}
// 默认使用 Cas20ServiceTicketValidator 来进行通信,跟前端调后端接口一样
TicketValidator ticketValidator = ensureTicketValidator();
try {
// step8. 来这里吧,要开始跟cas服务器通信了,验证下token的正确性
// 这个过程就不说了,建议自己debug进去看看,是怎么通信的,我在项目里写了这个模拟,可以看看
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
Map
// refresh authentication token (user id + remember me)
casToken.setUserId(userId);
String rememberMeAttributeName = getRememberMeAttributeName();
String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
if (isRemembered) {
casToken.setRememberMe(true);
}
List
到这里差不多就结束了,具体使用可以参见网上使用方法,你也可以在代码里使用注解,shiro有自己的aop实现,他会把那些打注解的类,方法进行代理。
> 最后说下可以优化和注意的点:
1. WebSessionManager在每次获取session的时候都会从SessionDAO里面读取,如果缓存是redis,这样很消耗性能,最好重写retrieveSession这个方法,将第一次获取到的Session存放到request里面去,后面每次从这里面取。
2. 就是使用Redis将Session序列化存储的时候,SimpleSession里面字段都是transient修饰的,选择序列化方案时,请注意。要么自己重写SimpleSession,要么选一个不会忽略transient的序列化方式。
3. 那些不需要认证的资源跟需要认证的资源一样都会从 SessionDAO获取一次Session,其实这个完全没必要,可以,这个也可以在WebSessionManager里面进行优化。
4. 不管什么请求发送到服务器,服务器都会先把请求生成一个会话保存到 会话存储的地方,如果有人一直请求,会造成 会话存储跑满,最终造成拒绝服务攻击。(解决办法,将没有认证的会话和认证过的会话放在不同的地方,也可以不保存没有认真的会话,但不保存会导致用户第一次登录认证,不会导航到用户第一次访问的那个地址,而使原先设定好的地址,这会影响用户体验)
5.
License
-------
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)