关于java:Shiro全解析又帅又能打

54次阅读

共计 8791 个字符,预计需要花费 22 分钟才能阅读完成。

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;

  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 管理器,

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 就认出是你这个用户了。

正文完
 0