shiro 的入门应用
简略说下原理:
认证:调用登录接口后,在 CustomRealm 的 doGetAuthenticationInfo()办法中,通过用户名查问到数据库中的明码,与登录接口中传入的明码比照,相等则认证胜利,返回 Cookie。
受权:调用有权限注解的接口如 /test, 在 CustomRealm 的 doGetAuthorizationInfo()办法中,通过用户名查问到数据库中的用户权限,与以后须要的权限比照,匹配则受权胜利,能够持续拜访。
Controller 层
@RestController
public class TestController {@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login(){Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("liaowh","123");
subject.login(token);
return "login";
}
@RequestMapping(value = "/home",method = RequestMethod.GET)
public String home(){return "home";}
@RequiresRoles("user")
@RequestMapping(value = "/test",method = RequestMethod.GET)
public String test(){return "test";}
Shiro 配置类
@Configuration
public class ShiroConfig {
@Bean
public CustomRealm customRealm() {CustomRealm customRealm = new CustomRealm();
return customRealm;
}
@Bean
public DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(customRealm());
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login", "anon");
// 除登录页面外的其余页面都须要认证
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
自定义 Realm
public class CustomRealm extends AuthorizingRealm {private Logger logger = LoggerFactory.getLogger(getClass());
// 受权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {logger.info("doGetAuthorizationInfo");
// 从主体传过来的认证信息中,获取用户名
String userName = (String)principalCollection.getPrimaryPrincipal();
// 通过用户名从数据库中获取角色数据
Set<String> roles = getRolesByUserName(userName);
// 通过用户名从数据库中获取权限数据
Set<String> permissions = getPermissionsByUserName(userName);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
// 认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {logger.info("doGetAuthenticationInfo");
// 从主体传过来的认证信息中,获取用户名
String userName = (String) authenticationToken.getPrincipal();
// 通过用户名从数据库中取得明码
String password = getPassWordByUserName();
if(password == null){return null;}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,getName());
return authenticationInfo;
}
private String getPassWordByUserName() {return "123";}
private Set<String> getPermissionsByUserName(String userName) {Set<String> sets = new HashSet<String>();
sets.add("user:add");
return sets;
}
private Set<String> getRolesByUserName(String userName) {Set<String> sets = new HashSet<String>();
sets.add("user");
return sets;
}
}
Shiro 源码解析之认证
认证(Authentication):
身份验证的过程,也就是证实一个用户的实在身份。为了证实用户身份,须要提供零碎了解和置信的身份信息和证据。须要通过向 Shiro 提供用户的身份(principals)和证实(credentials)来断定是否和零碎所要求的匹配。
Principals(身份)
是 Subject 的“标识属性”,能够是任何与 Subject 相干的标识,比如说名称(给定名称)、名字(姓或者昵称)、用户名、平安号码等等。
Primary Principals(次要身份)
尽管 Shiro 容许用户能够应用多个身份,然而还是心愿用户能有一个精准表明用户的身份,一个仅有的惟一标识 Subject 值。在少数程序中常常会是一个用户名、邮件地址或者全局惟一的用户 ID。
Credentials(证实)
通常是只有 Subject 本人才晓得的秘密内容,用来证实 Subject 真正领有所需的身份。一些简略的证书例子如明码、指纹等。最常见的身份 / 证实是用户名和明码,用户名是所需的身份阐明,明码是证实身份的证据。如果一个提交的明码和零碎要求的统一,程序就认为该用户身份正确,因为其他人不应该晓得同样的明码。
调用 login 接口,subject.login(token)向 Authentication 提交身份和证实
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login(){Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("liaowh","123");
subject.login(token);
return "login";
}
调用 securityManager.login()
以后只配置了一个 realm,执行 doSingleRealmAuthentication()
调用咱们自定义 realm 的 doGetAuthenticationInfo()办法
关键处:
来自 subject 提交的 token 和来自自定义 realm 的 info 的明码比照,此时前者明码来自申请,后者来自数据库
equals 办法对 token 的明码和从数据库中取出来的 info 中的明码进行比照,如果认证雷同就返回 true,否则将抛出异样
Shiro 源码解析之受权
受权提供的 API 很多,上面解析下罕用的 hasRole()和 isPermitted()办法
hasRole():
isPermitted():
Shiro 受权也能够用注解形式,Shiro 权限注解基于编程式的 SpringAop 实现。
几个相干概念:
Advistor:相当于 Aspect, 由切入点 Pointcut 和告诉 Advice 组成。
Advice:告诉,定义了切面的工作是什么以及何时应用。
MethodInterceptor:盘绕加强。
上面大佬写的很好了,我就不班门弄斧了。
shiro 注解受权源码实现
shiro 源码解析之 Filter 初始化
ShiroFilterFactoryBean 实现了 FactoryBean 接口,那么 Spring 在初始化的时候必然会调用 ShiroFilterFactoryBean 的 getObject()获取实例。
进入 createInstance()办法
这里次要有重要的两个步骤
1. 通过createFilterChainManager()
办法创立一个 FilterChainManager
进入 createFilterChainManager()看看
第一步中的 new DefaultFilterChainManager()初始化了一个 LinkHashMap 类型的 filters, 而后通过 addFilter 办法将 Shiro 默认的过滤器增加进该 filters
在 applyGlobalPropertiesIfNecessary()
办法中将某些默认 filter 中的 loginUrl,SuccessUrl,UnauthorizedUrl 替换成咱们设置的值。
下一步,Map<String, Filter> filters = getFilters(); 这里是获取咱们自定义的过滤器,默认是为空的,如果咱们配置了自定义的过滤器,那么会将其增加到 filters 中。至此 filters 中蕴含着 Shiro 内置的过滤器和咱们配置的所有过滤器。
再下一步,遍历 filterChainDefinitionMap,这个 filterChainDefinitionMap 就是咱们在 ShiroConfig 中注入进去的拦挡规定配置。
chainName 是咱们配置的过滤门路,chainDefinition 是该门路对应的过滤器,这里阐明咱们能够为一个门路,同时配置多个过滤器。splitChainDefinition(chainDefinition)
办法会将 chainDefinition 中的过滤器分离出来与门路对应。
过滤门路和过滤器是一对多的关系,所以 ensureChain()返回的 NamedFilterList 其实就是一个有着 name 为 Key 的 List<Filter>,这个 name 保留的就是过滤门路,List 保留着咱们配置的过滤器。获取到 NamedFilterList 后在将过滤器退出其中,这样过滤门路和过滤器映射关系就初始化好了。
2. 将这个 FilterChainManager 注入 PathMatchingFilterChainResolver 中, 它是一个过滤器执行链解析器。
咱们每次申请服务器都会调用这个办法,依据申请的 URL 去匹配过滤器执行链中的过滤门路,匹配上了就返回其对应的过滤器进行过滤。
Shiro 源码解析之 Session
首先说下 HttpSession 的原理,不然像我一样没用过 session 的一脸懵逼:
当客户端第一次拜访服务器的时候,此时客户端的申请中不携带任何标识给服务器,所以此时服务器无奈找到与之对应的 session,所以会新建 session 对象,当服务器进行响应的时候,服务器会将 session 标识放到响应头的 Set-Cookie 中,会以 key-value 的模式返回给客户端. 例:JSESSIONID=7F149950097E7B5B41B390436497CD21;其中 JSESSIONID 是固定的,而前面的 value 值对应的则是给该客户端新创建的 session 的 ID,之后浏览器再次进行服务器拜访的时候,客户端会将此 key-value 放到 cookie 中一并申请服务器,服务器就会依据此 ID 寻找对应的 session 对象了;(当浏览器敞开后,会话完结,因为 cookie 隐没所以对应的 session 对象标识隐没,而对应的 session 仍然存在,但曾经成为报废数据期待 GC 回收了)。对应 session 的 ID 能够利用此办法失去:session.getId();
Shiro 中的 Session
Shiro 提供了残缺的会话治理性能,不依赖底层容器,JavaSE 利用和 JavaEE 利用都能够应用。SessionManager(会话管理器)治理着利用中所有 Subject 的会话,包含会话的创立、保护、删除、生效、验证等工作。
SessionManager 接口
public interface SessionManager {Session start(SessionContext context);
Session getSession(SessionKey key) throws SessionException;
}
SessionManager
接口是 Shiro 所有会话管理器的顶级接口。在此接口中申明了两个办法 Session start(SessionContext context);
和Session getSession(SessionKey key) throws SessionException;
。
Session start(SessionContext context);
办法,基于指定的上下文初始化数据启动新会话。Session getSession(SessionKey key) throws SessionException;
依据指定的SessionKey
检索会话,如果找不到则返回null
。如果找到了会话,但会话但有效(已进行或已过期)则抛出SessionException
异样。
Shiro 的 web 环境下的 Session 治理
ServletContainerSessionManager
是基于 spring web 实现的,只能用于 Web 环境下,它只是简略的封装了 Serlvet
相干性能。DefaultWebSessionManager
是 shrio 反对用于 Web 环境下,只不过应用了本人的 session 治理。
ServletContainerSessionManager
ServletContainerSessionManager 是 Shiro 默认的 session 管理器,
public class ServletContainerSessionManager implements WebSessionManager {
// 实现 start 接口,创立 Session 实例
public Session start(SessionContext context) throws AuthorizationException {return createSession(context);
}
protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
// 这里 sessionContext 理论是 DefaultWebSessionContext 类型,从中提取 HttpServletRequest
HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
// 从中提取 HttpSession
HttpSession httpSession = request.getSession();
// 构建 HttpServletSession 实例
String host = getHost(sessionContext);
return createSession(httpSession, host);
}
protected Session createSession(HttpSession httpSession, String host) {return new HttpServletSession(httpSession, host);
}
// 获取 session
public Session getSession(SessionKey key) throws SessionException {HttpServletRequest request = WebUtils.getHttpRequest(key);
Session session = null;
// 从 HttpServletRequest 获取 session
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {session = createSession(httpSession, request.getRemoteHost());
}
return session;
}
}
ServletContainerSessionManager 自身并不治理会话,它最终操作的还是 HttpSession,所以只能在 Servlet 容器中起作用,它不能反对除应用 HTTP 协定的之外的任何会话。HttpServletSession 持有 servlet 的 HttpSession 的援用,最终对 HttpServletSession 的操作都会委托给 HttpSession(装璜模式)
ServletContainerSessionManager 创立 session 的调用链如下时序图,能够看出 Subject.login()登录胜利后用户的认证信息实际上是保留在 HttpSession 中的
DefaultWebSessionManager
DefaultWebSessionManager 的 Session 创立与存储时序图:
Session 的最终创立在 SimpleSessionFactory 的 createSession
sessionId 的创立在 EnterpriseCacheSessionDAO 的 generateSessionId 办法中,在 assignSessionId 办法外面存储在 session 中
后续等 session 创立完返回后,会将 sessionId 放入 Cookie 中
以上是 shiro 应用 DefaultWebSessionManager,在登录的时候创立了 session sessionId 等,那么当下一个申请到服务器的时候,shiro 是怎么辨认出以后是哪个用户的呢?
首先申请进入 OncePerRequestFilter,而后进入到 AbstractShiroFilter 的 doFilterInternal()办法如下:
createSubject()办法最终走到:
debug 发现走到这里
进去瞧一瞧,如果以后申请是带了登录返回的 Cookie, 那么 sessionId 必然不为 null, 那么 getSessionKey()返回的 key 也不为 null,即进入 getSession(key)办法
从以下两图能够看出,这个 this.sessionManager.getSession(key)必定是在 AbstractNativeSessionManager 实现的,因为上面两个一个不再继承体系里,一个就是本人自身
来到这个这里,看到 doxx 的办法应该才是干活的
debug 发现 getSessionId() 被 DefaultWebSessionManager 笼罩
看到这里应该终于懂了吧,就是取出申请中的 cookie 的 id,回到这幅图,拿到了 id 有啥用啊,当然是要通过 id 拿用户的 session,retrieveSessionFromDataSource,进去看看呗
最终从 sessionDAO(默认是 MemorySessionDAO)中获取 session
而后呢,我都差点绕晕了你敢信,回到这两幅图
最终在 save(subject)办法中,将 subject 保留到了 session 中。
总结下这个流程,登录之后返回带了 jsessionId 的 Cookie 给前端,前端下次再带上这个 Cookie,Shiro 从 Cookie 中拿到 jsessionId,而后通过这个 id 能拿到 session,那么 shiro 就认出是你这个用户了。