在Springboot中使用Shiro
ShiJh Lv3

Shiro总体来说还是很轻量的,也能够轻松的在Springboot项目中使用。

SpringBoot-Shiro

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.7.1</version>
</dependency>

💬带web描述的依赖表示集成Web环境

简介

Shiro属于安全框架,通过Shiro可以轻松的实现登录认证、鉴权、授权、加密等常见的安全操作,Shiro框架的核心是SecurityManagerRealmSubject,理解三者的关系以及作用即可完成Shiro的快速入门!

  • SecurityManager

    框架的核心控制器,相当于Spring-mvc中的DispatcherManager,调度各种Realm并使用Authenticator进行认证和鉴权。

  • Realm

    这是应用开发人员与框架的桥梁,最重要的的作用是向Shiro传递用户信息,包括账号密码,权限,角色也就是所谓的授权,用户的信息需要我们从数据库中获取,因此这个组件是我们使用Shiro中主要的自定义对象。

  • Subject

    框架的使用入口,也相当于是一个会话对象Session,每个不同的用户访问Shiro都会提供不同的Subject,使用SecurityUtils进行获取。

执行流程图

Shiro流程图

快速入门

核心配置

在SpringBoot使用Shiro需要三个完成最低限度的配置,分别是ShiroFilterChainDefinitionSessionsSecurityManagerDefaultAdvisorAutoProxyCreator,在@Configuration类中配置以下Bean

  • SessionsSecurityManager

    @Bean
    public SessionsSecurityManager securityManager(@Autowired Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置自定义的Realm
        securityManager.setRealm(realm);
        return securityManager;
    }
  • DefaultAdvisorAutoProxyCreator

    设置两个属性为true,解决Shiro与SpringAop的兼容问题

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new 			DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
  • ShiroFilterChainDefinition

    为Shiro自带的Filter配置Pattern,能够配置多个,这是一个很重要的配置,关系到用户鉴权

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        // 配置所有请求路径都可以匿名访问 anno属于Shiro默认的过滤器
        // 过滤顺序由添加的先后顺序决定
        chain.addPathDefinition("/**", "anon");
        return chain;
    }

编写基础Realm

Shiro内部实现了很多Realm,最主要的一个是AuthorizingRealm,因此我们自定义的对象也是它。

@Component
public class Realm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) 	{
        User user = (User) principalCollection.getPrimaryPrincipal();
        //添加权限信息(角色、权力)
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(user.getRoleList());
        authorizationInfo.addStringPermission(user.getAllPermission());
        return authorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        User user = userService.findByName(username);
        if (user == null) return null;	//表示UnknownAccount异常
        //返回认证信息
        return new SimpleAuthenticationInfo (
            user, user.getPassword(), getName()
        );
    }
}

💬第一次看到这个类一定会一头雾水,但是详细解读后将变得非常简单。

实现鉴权

鉴权分为两条路径,一条是角色鉴定另一条是权力鉴定,一般我们使用Filter来鉴定角色,注解来鉴定权力。

首先是使用注解@RequirePermission(value,logic)鉴定权限,value为权限名即上文代码中所述的StringPermission,属于数组,能够指定多个,logic是一个枚举,仅有ANDOR两种,表示多个value之间是还是的关系,默认为AND

@RequirePermission({"admin:read","admin:write"},Logic.AND)
public Object controllerMethod(){}

其次是Filter,在上文中配置了/**表示任意路径,anon为匿名过滤器,允许未认证用户访问。还有其他过滤器以及我们自定义的过滤器都使用这种方式来配置,过滤器能通过用户所具有角色进行角色鉴定!

💬无论是角色还是权限都能使用 Filter和注解,不过各有各的方便之处

Shiro原理解析

Subject

代表当前会话的对象,从SecurityUtils中能够获得,最主要的方法就是login(token)方法,执行该方法后将进入Shiro框架的SecurityManager对象中,此时将会调用Realm中的doGetAuthenticationInfo()并传入从Login方法中获取的token进行验证。Login之后,当前会话的Subject中就会保存一个Principal,通过getPrincipal()方法获取。

Realm

doGetAuthenticationInfo方法用来验证传入的Token信息是否合法,判断逻辑由我们来编写,失败需要返回null,而成功需要返回认证信息,认证信息包括Principalcredentials、当前使用的RealmName以及用于MD5加密credentialssalt

Shrio能够通过配置使用MD5加密Credentials

对于credentialsSalt,在Shiro中为org.apache.shiro.util.ByteSource对象:
ByteSource credentialsSalt = ByteSource.Util.bytes("");
ByteSource提供了一个内部方法,可以将字符串转换为对应的盐值信息。一般情况下我们使用一个唯一的字符串作为盐值,这个salt需要同用户信息保存到数据库,再Realm中验证时需要使用同一个salt加密Token中的credential后进行比较验证。

验证通过后会调用*doGetAuthorizationInfo()将当前会话的PrimaryPrincipalprincipalCollection中取出,PrimaryPrincipal 由 doGetAuthenticationInfo 方法创建认证信息时指定。与Subject.Login传入的Token中的Principle不同,PrimaryPrincipal 将会代表整个会话。在这个阶段需要编写角色和权限获取的逻辑,并保存到一个授权信息对象中返回给SecurityManager

⚠️Shiro使用单个Realm和多个Realm的策略是不同的,其中对异常的处理也是不同的,多Realm在认证失败的时候会很坑爹的抛出找不到对应Realm的异常,无法抛出自定义异常,详细情况和勉强的解决方法可以看这篇文章

ℹ️通过重写ModularRealmAuthenticatordoMultiRealmAuthentication可以改变多Realm调用策略,一种较好的方法是根据Token的类型使用唯一确定的Realm,然后调用doSingleRealmAuthentication方法

Filter

FIlter是Shiro框架组成中的一个关键部分,Shiro所有鉴权和认证都是通过Filter进行的,内部实现了许多默认的Filter供开发人员配置,但是通过重写Shiro的FIlter类能够实现一套自定义的认证或鉴权逻辑,一般都会用来实现不同的认证,如token认证。

Filter详解

常用过滤器

过滤器名 作用 对应注解
anon 允许匿名访问
authc 只允许认证用户访问 @RequiresAuthentication
roles 只允许指定角色访问 @RequiresRoles
user 存在用户信息才可以访问 @RequiresUser
perms 只允许拥有指定权限才能访问 @RequiresPermission
logout 清空访问用户的认证授权信息

⚠️rolesperms字符串末尾连接多个[name]来表示指定的角色或权限。

InvalidRequestFilter

这是一个Shiro在springboot中默认配置的全局过滤器,作用是拦截过滤非法字符的Url,由于地方原因,这个过滤器会直接把中文字符是为非法,从而拦截请求,返回错误代码400。然而这种情况并不会影响有中文的QueryParameter请求,因为在过滤阶段,Shiro InvalidRequestFilter 将验证三种URL,其中我们可以从浏览器看到的Url中文会经过Url编码因此不会受到过滤器影响,而第二种则是来自Tomcat内部的ServerUrl这个表示除去QueryParameter的部分,且没有进行UrlDecode对于普通的Get请求不存在中文映射则放行,而对于RESTFul风格的请求,如果有中文则会被拦截!

防止中文Restful请求被拦截

从源码分析得出,只要替换掉InvalidRequestFilter则可以解决该问题。

为了最大程度保留InvalidRequestFilter替我们实现好的的功能,我们直接复制一份源码修改其中用于判断字符的关键代码,然后配置到ShiroFilterFactoryBean中的globalFilter即可。

贴出与原过滤器不同的部分,完整代码移步 GithubGist 复制粘贴。

@Component
public class CNInvalidRequestFilter extends AccessControlFilter {
    //添加一个新的方法用于判断 Chinese Char
    private static boolean isChinese(char c) {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
        return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
            || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
            || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
            || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
            || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
            || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
            || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION;
    }
    private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
        int length = uri.length();
        for(int i = 0; i < length; ++i) {
            char c = uri.charAt(i);
            //将中文加入判断条件中
            if ((c < ' ' || c > '~') && !isChinese(c)) {
                return false;
            }
        }
        return true;
    }

}

配置ShiroFilterFactoryBean

public ShiroFilterFactoryBean getShiroFilterFactoryBean(CNInvalidRequestFilter invalidRequestFilter) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //...省略其他必须配置项,Cl为自用工具类,意思意思即可
    shiroFilterFactoryBean.setFilters(
        Cl.map().add("invalid", invalidRequestFilter).castBuild()
    );
    shiroFilterFactoryBean.setGlobalFilters(
        Cl.list("invalid")
    );
    return shiroFilterFactoryBean;
}

自定义Filter

最常见的是实现类AuthenticatingFilter,通过重写其onAccessDeniedcreateToken即可实现一个不同于Shiro默认的认证逻辑。

推荐一篇文章,对Shiro各大Filter都进行了详细解读

具体讲一讲AuthenticatingFilter这个认证过滤器。它默认实现了一个从请求中进行登录的功能 executeLogin(resq,resp)利用这个功能,我们能在用户会话失效认证失败后尝试进行自动登录,也就是再onAccessDenied中调用它,它将返回一个是否登录成功的布尔值,其中执行的逻辑是调用createTokenrequest或者response获取Token进入Realm中验证,因此我们可以利用这个性质在前后端通过一个唯一对应的值进行认证!也就是我们经常在抓包中经常见到的token字符串

onAccessDenied方法会在默认的isAccessAllow返回False后调用作进一步的判断。默认的isAccessAllow其实是沿用了AccessControlFilter中的方法——对当前Subject对象进行判断是否处于认证状态来决定是否放行——并添加了一条isLoginRequest方法进行判断,该方法判断请求的连接是否是用于登录的连接,如果是则会放行,如果不是则最终调用onAccessDenied进行判断。

详细流程图

Shiro流程图

 评论