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框架的核心是SecurityManager
、Realm
和Subject
,理解三者的关系以及作用即可完成Shiro的快速入门!
-
SecurityManager
框架的核心控制器,相当于Spring-mvc中的
DispatcherManager
,调度各种Realm
并使用Authenticator进行认证和鉴权。 -
Realm
这是应用开发人员与框架的桥梁,最重要的的作用是向Shiro传递用户信息,包括账号密码,权限,角色也就是所谓的授权,用户的信息需要我们从数据库中获取,因此这个组件是我们使用Shiro中主要的自定义对象。
-
Subject
框架的使用入口,也相当于是一个会话对象
Session
,每个不同的用户访问Shiro都会提供不同的Subject,使用SecurityUtils
进行获取。
○执行流程图
○快速入门
○核心配置
在SpringBoot使用Shiro需要三个完成最低限度的配置,分别是ShiroFilterChainDefinition
、SessionsSecurityManager
、DefaultAdvisorAutoProxyCreator
,在@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是一个枚举,仅有AND
、OR
两种,表示多个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,而成功需要返回认证信息,认证信息包括Principal
、credentials
、当前使用的RealmName
以及用于MD5加密credentials的salt
。
Shrio能够通过配置使用MD5加密Credentials
对于credentialsSalt,在Shiro中为
org.apache.shiro.util.ByteSource
对象:
ByteSource credentialsSalt = ByteSource.Util.bytes("");
ByteSource提供了一个内部方法,可以将字符串转换为对应的盐值信息。一般情况下我们使用一个唯一的字符串作为盐值,这个salt需要同用户信息保存到数据库,再Realm中验证时需要使用同一个salt加密Token中的credential后进行比较验证。
验证通过后会调用*doGetAuthorizationInfo()
将当前会话的PrimaryPrincipal从principalCollection中取出,PrimaryPrincipal 由 doGetAuthenticationInfo 方法创建认证信息时指定。与Subject.Login
传入的Token中的Principle不同,PrimaryPrincipal 将会代表整个会话。在这个阶段需要编写角色和权限获取的逻辑,并保存到一个授权信息对象中返回给SecurityManager
⚠️Shiro使用单个Realm和多个Realm的策略是不同的,其中对异常的处理也是不同的,多Realm在认证失败的时候会很坑爹的抛出找不到对应Realm的异常,无法抛出自定义异常,详细情况和勉强的解决方法可以看这篇文章
ℹ️通过重写
ModularRealmAuthenticator
的doMultiRealmAuthentication
可以改变多Realm调用策略,一种较好的方法是根据Token的类型使用唯一确定的Realm,然后调用doSingleRealmAuthentication
方法
○Filter
FIlter是Shiro框架组成中的一个关键部分,Shiro所有鉴权和认证都是通过Filter进行的,内部实现了许多默认的Filter供开发人员配置,但是通过重写Shiro的FIlter类能够实现一套自定义的认证或鉴权逻辑,一般都会用来实现不同的认证,如token认证。
○Filter详解
○常用过滤器
过滤器名 | 作用 | 对应注解 |
---|---|---|
anon | 允许匿名访问 | 无 |
authc | 只允许认证用户访问 | @RequiresAuthentication |
roles | 只允许指定角色访问 | @RequiresRoles |
user | 存在用户信息才可以访问 | @RequiresUser |
perms | 只允许拥有指定权限才能访问 | @RequiresPermission |
logout | 清空访问用户的认证授权信息 | 无 |
⚠️
roles
和perms
字符串末尾连接多个[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
,通过重写其onAccessDenied
、createToken
即可实现一个不同于Shiro默认的认证逻辑。
推荐一篇文章,对Shiro各大Filter都进行了详细解读
具体讲一讲AuthenticatingFilter
这个认证过滤器。它默认实现了一个从请求中进行登录的功能 executeLogin(resq,resp)
利用这个功能,我们能在用户会话失效认证失败后尝试进行自动登录,也就是再onAccessDenied中调用它,它将返回一个是否登录成功的布尔值,其中执行的逻辑是调用createToken从request
或者response
获取Token进入Realm中验证,因此我们可以利用这个性质在前后端通过一个唯一对应的值进行认证!也就是我们经常在抓包中经常见到的token字符串
onAccessDenied
方法会在默认的isAccessAllow
返回False后调用作进一步的判断。默认的isAccessAllow
其实是沿用了AccessControlFilter
中的方法——对当前Subject对象进行判断是否处于认证状态来决定是否放行——并添加了一条isLoginRequest
方法进行判断,该方法判断请求的连接是否是用于登录的连接,如果是则会放行,如果不是则最终调用onAccessDenied进行判断。