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就认出是你这个用户了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。