3

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()
QQ20200716-155331@2x.png

QQ20200716-155357@2x.png

QQ20200716-155412@2x.png

QQ20200716-155511@2x.png

当前只配置了一个realm,执行doSingleRealmAuthentication()
QQ20200716-155843@2x.png

调用我们自定义realm的doGetAuthenticationInfo()方法
QQ20200716-162356@2x.png

关键处:
来自subject提交的token和来自自定义realm的info的密码对比,此时前者密码来自请求,后者来自数据库
QQ20200716-160316@2x.png

QQ20200716-160518@2x.png

equals方法对token的密码和从数据库中取出来的info中的密码进行对比,如果认证相同就返回true,否则将抛出异常
QQ20200716-160532@2x.png

Shiro源码解析之授权

授权提供的API很多,下面解析下常用的hasRole()和isPermitted()方法
WX20200717-104804@2x.png
hasRole():
WX20200717-110659@2x.png

WX20200717-110757@2x.png

WX20200717-110949@2x.png

isPermitted():
WX20200717-110722@2x.png

WX20200717-110818@2x.png

WX20200717-111108@2x.png

Shiro授权也可以用注解方式,Shiro权限注解基于编程式的SpringAop实现。
几个相关概念:
Advistor:相当于Aspect,由切入点Pointcut和通知Advice组成。
Advice:通知,定义了切面的工作是什么以及何时使用。
MethodInterceptor:环绕增强。
下面大佬写的很好了,我就不班门弄斧了。
shiro注解授权源码实现

shiro源码解析之Filter初始化

ShiroFilterFactoryBean实现了FactoryBean接口,那么Spring在初始化的时候必然会调用ShiroFilterFactoryBean的getObject()获取实例。
WX20200728-103254@2x.png
进入createInstance()方法
WX20200728-101441@2x.png
这里主要有重要的两个步骤
1.通过createFilterChainManager()方法创建一个FilterChainManager
进入createFilterChainManager()看看
WX20200728-101527@2x.png
第一步中的new DefaultFilterChainManager()初始化了一个LinkHashMap类型的filters,然后通过addFilter方法将Shiro默认的过滤器添加进该filters
WX20200728-104303@2x.png
WX20200728-104437@2x.png
applyGlobalPropertiesIfNecessary()方法中将某些默认filter中的loginUrl,SuccessUrl,UnauthorizedUrl替换成我们设置的值。WX20200728-101710@2x.png
下一步,Map<String, Filter> filters = getFilters(); 这里是获取我们自定义的过滤器,默认是为空的,如果我们配置了自定义的过滤器,那么会将其添加到filters中。至此filters中包含着Shiro内置的过滤器和我们配置的所有过滤器。
再下一步,遍历filterChainDefinitionMap,这个filterChainDefinitionMap就是我们在ShiroConfig中注入进去的拦截规则配置。
WX20200728-101854@2x.png
chainName是我们配置的过滤路径,chainDefinition是该路径对应的过滤器,这里说明我们可以为一个路径,同时配置多个过滤器。splitChainDefinition(chainDefinition)方法会将chainDefinition中的过滤器分离出来与路径对应。
WX20200728-102521@2x.png
过滤路径和过滤器是一对多的关系,所以ensureChain()返回的NamedFilterList其实就是一个有着name为Key的List<Filter>,这个name保存的就是过滤路径,List保存着我们配置的过滤器。获取到NamedFilterList后在将过滤器加入其中,这样过滤路径和过滤器映射关系就初始化好了。
WX20200728-102653@2x.png

2.将这个FilterChainManager注入PathMatchingFilterChainResolver中,它是一个过滤器执行链解析器。
我们每次请求服务器都会调用这个方法,根据请求的URL去匹配过滤器执行链中的过滤路径,匹配上了就返回其对应的过滤器进行过滤。
WX20200728-111906@2x.png

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的会话,包括会话的创建、维护、删除、失效、验证等工作。
image.png

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;

  1. Session start(SessionContext context);方法,基于指定的上下文初始化数据启动新会话。
  2. 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管理器,
image.png

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中的
image.png

DefaultWebSessionManager

DefaultWebSessionManager的Session创建与存储时序图:
image.png

Session的最终创建在SimpleSessionFactory的createSession
image.png

sessionId的创建在EnterpriseCacheSessionDAO的generateSessionId方法中,在assignSessionId方法里面存储在session中
image.png
image.png
后续等session创建完返回后,会将sessionId放入Cookie中
image.png
image.png
以上是shiro使用DefaultWebSessionManager,在登录的时候创建了session sessionId等,那么当下一个请求到服务器的时候,shiro是怎么识别出当前是哪个用户的呢?

首先请求进入OncePerRequestFilter,然后进入到AbstractShiroFilter的doFilterInternal()方法如下:
image.png
createSubject()方法最终走到:
image.png
debug发现走到这里
image.png
进去瞧一瞧,如果当前请求是带了登录返回的Cookie,那么sessionId必然不为null,那么getSessionKey()返回的key也不为null,即进入getSession(key)方法
image.png
从以下两图可以看出,这个this.sessionManager.getSession(key)肯定是在AbstractNativeSessionManager实现的,因为下面两个一个不再继承体系里,一个就是自己本身
image.png
image.png
来到这个这里,看到doxx的方法应该才是干活的
image.png
image.png
image.png
debug发现getSessionId()被DefaultWebSessionManager覆盖
image.png
image.png
image.png
image.png
image.png
看到这里应该终于懂了吧,就是取出请求中的cookie的id,回到这幅图,拿到了id有啥用啊,当然是要通过id拿用户的session,retrieveSessionFromDataSource,进去看看呗
image.png
最终从sessionDAO(默认是MemorySessionDAO)中获取session
image.png
然后呢,我都差点绕晕了你敢信,回到这两幅图
image.png
image.png
最终在save(subject)方法中,将subject保存到了session中。
总结下这个流程,登录之后返回带了jsessionId的Cookie给前端,前端下次再带上这个Cookie,Shiro从Cookie中拿到jsessionId,然后通过这个id能拿到session,那么shiro就认出是你这个用户了。


WillLiaowh
71 声望8 粉丝

世界上最伟大的力量是坚持。