Shiro框架

Shiro

  • Apache Shiro是Java的一个安全框架.
    • 用于身份认证,,授权,加密和会话管理与WEB集成,缓存.

Shiro架构

屏幕快照 2019-06-03 下午11.43.52

组件 作用
Subject 当前访问系统的用户,进行认证的主体.getSubject()获取当前用户,委托Shiro实现功能.
SecurityManager 安全管理器,负责与shiro的其他组件进行交互,实现Subject委托的各种功能.
Realms 数据源,Realms是shiro与数据库之间的连接器,shiro从自定义的Realms查找相关的数据进行比对,检验.
Authenticator 认证器,用于协调协调一个或者多个Realm,从Realm指定的数据源取得数据之后,进行认证.
Authorizer 授权器,决定用户是否拥有执行指定操作的权限.
SessionManager 会话管理
CacheManager 缓存组件,用于缓存认证信息等
Cryptography Shiro提供了一个加解密的命令行工具jar包

shiro构架核心

  • Subject
  • SecurityManager
  • Realms

屏幕快照 2019-06-03 下午11.58.29

用户登录认证—>Authenticator

访问授权—>Authorizer

Shiro认证

依赖包

作用
shiro-core shiro核心
shiro-web shiro的Web模块
shiro-spring shiro和Spring集成
shiro-ehcache shiro底层使用的ehcache缓存

认证流程图:

屏幕快照 2019-06-04 上午12.05.01

认证操作

配置shiro.ini文件,用于被shiro读取ini里配置的用户信息

使用shiro相关Api进行认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testShiro() {

//1. 加载shiro.ini配置文件,得到配置中的用户信息(账号+密码)
IniSecurityManagerFactory factory = new
IniSecurityManagerFactory("classpath:shiro.ini");

//2.创建Shiro安全管理器对象
SecurityManager instance = factory.getInstance();

//3.将创建的安全管理器添加到运行环境中,告诉shiro使用哪个安全管理器
SecurityUtils.setSecurityManager(instance);

//4.获取登录的用户主体对象
Subject subject = SecurityUtils.getSubject();

//创建用户登录的身份凭证,封装令牌对象,携带用户数据
UsernamePasswordToken token = new UsernamePasswordToken("haha", "555");

System.out.println("登录前的认证状态"+subject.isAuthenticated());
//登录认证,将用户的和ini配置中的账号密码做匹配
subject.login(token);
System.out.println("登录前的认证状态"+subject.isAuthenticated());
}
  • SecurityUtils.getSubject();
    • 获取用户主体对象,调用login方法委托shiro进行登录的认证操作.
  • factory.getInstance();
    • 创建安全管理器对象
    • 添加到运行环境,告诉shiro使用哪个安全管理器调用Authenticator进行认证操作

异常信息

UnknownAccountException** —> 账号错误

IncorrectCredentialsException—> 密码错误

shiro认证流程分析

屏幕快照 2019-06-04 上午12.27.58

  • 在DelegatingSubject,调用login()方法,传入当前的subject对象和封装好的用户令牌.
  • 在DefaultSecurityManager方法中,如果认证方法(令牌)失败.则抛出异常,调用onFailedLogin方法.成功则调用onSuccessfulLogin方法,传入token
  • 在AbstractAuthenticator中,调用doAuthenticate方法,传入token
  • 在ModularRealmAuthenticator中,getRealms拿到数据源,判断数据源个数调用不同的方法
  • AuthenticatingRealm,根据账户信息doGetAuthenticationInfo(重写),拿到用户的认证信息.然后做账户的认证和密码的认证.
  • 在SimpleAccountRealm根据token.username获取账户对象.被锁,凭证过期抛异常,最终返回账户信息给调用者.然后继续验证密码.
  • 在SimpleCredentialsMatcher拿到凭证匹配器,拿到token和账户对象比对
  • 有异常回到onFailedLogin,没有异常onSuccessfulLogin

自定义Realm

  • 需要的账户信息通常来自程序或数据库中,需要自定义Realm.
  • 继承AuthorizingRealm(需要缓存,认证,授权所有的功能)
  • 重写doGetAuthenticationInfo()方法
  • 返回AuthenticationInfo对象

获取数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取认证信息
//authenticationToken:用户的账号密码信息对象
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

//获取用户账号
String username = (String) authenticationToken.getPrincipal();
//根据用户的账号去数据库查询真实的用户信息
Employee currentEmp = employeeMapper.selectByName(username);

if (currentEmp != null) {
//currentEmp:作为身份对象使用,因为在后面需要使用到当前登录用户的信息
return new SimpleAuthenticationInfo(
currentEmp,//身份信息
currentEmp.getPassword(),//凭证
ByteSource.Util.bytes(currentEmp.getName()),//盐
this.getName()//Realm名称
);
}
return null;
}

通过配置修改SecurityManager中的默认Realm的使用

1
2
3
4
#自定义Realm信息
CrmRealm=me.cscar.rbac.shiro.CRMRealm
#将CrmRealm设置到当前的环境中
securityManager.realms=$CrmRealm

预处理操作

  • 在认证之前,需要进行预处理.使用过滤器进行过滤操作.
  • 在web.xml上配置shiroFilter过滤器
1
2
3
4
5
6
7
8
9
10
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

shiro过虑器,DelegatingFilterProx会从spring容器中找shiroFilter,所以过滤器的生命周期
还是交给Spring进行管理的.

配置操作

为了方便对shiro的相关配置进行管理,分离出一个shiro.xml配置文件,在mvc.xml中引用

使用安全管理器DefaultWebSecurityManager,并且在该安全管理器中指定自定义的Realm

1
2
3
4
5
6
7
<!--配置安全管理器-->
<bean id="securityManager"
class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="CrmRealm"/>
<!--将自定义的Realm注入到安全管理器-->
<property name="cacheManager" ref="cacheManager"/>
</bean>

在shiro.xml中指定系统资源所需要的具体过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--引用指定的安全管理器-->
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/>
<property name="filterChainDefinitions">
<value>
/js/**=anon
/images/**=anon
/css/**=anon
/logout.do=logout
/**=authc
</value>
</property>
<!--设置当前使用的认证过滤器-->
<property name="filters">
<map>
<entry key="authc" value-ref="crmFormAuthenticationFilter">
</entry>
</map>
</property>
</bean>

过滤器

过滤器名称 作用
anon 匿名拦截器,即不需要登录即可访问.一般用于静态资源过滤
authc 表示需要认证(登录)才能使用
authcBasic Basic HTTP身份验证拦截器
roles 角色授权拦截器,验证用户是否拥有资源角色
perms 权限授权拦截器,验证用户是否拥有资源权限
rest rest风格拦截器,自动根据请求方法构建权限字符串
ssl SSL拦截器,只有请求协议是https才能通过.否则自动跳转会https端口

在自定义的Realm中使用注解,让安全管理器能够找到指定的Realm.

注解的目的是把自定义的Realm交给Spring管理

1
@Component("CrmRealm")

前端请求

在前端使用ajax发生异步请求的方式,需要后端返回一个json格式的数据

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
$(function(){
$("#btn_submit").click(function () {
$.post("/login.html", $("#loginForm").serialize(), function
(data) {
}) });
</script>
if(data.success){
window.location.href="/employee/list.do";
}else{ $.messager.alert("温馨提示",data.msg);
} })

后端的处理操作

  • Shiro默认会在FormAuthenticationFilter调用onLoginFailure()和onLoginSuccess()方法对认证的结果进行处理.
  • 因此需要继承FormAuthenticationFilter重写这两个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//登录成功处理
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {

response.setContentType("text/json;charset=UTF-8");

JsonResult jsonResult = new JsonResult();

response.getWriter().print(JSON.toJSONString(jsonResult));

return false;
}

//登录失败处理
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {

try {
response.setContentType("text/json;charset=UTF-8");

JsonResult jsonResult = new JsonResult();
jsonResult.mark("账号或密码错误");

response.getWriter().print(JSON.toJSONString(jsonResult));
} catch (IOException ex) {
ex.printStackTrace();
}

return false;
}

将继承自FormAuthenticationFilter的过滤器设置为当前使用的过滤器

1
2
3
4
5
6
7
<property name="filters">
<!--设置当前使用的认证过滤器-->
<map>
<entry key="authc" value-ref="crmFormAuthenticationFilter">
</entry>
</map>
</property>
  • Shiro封装了登录业务逻辑,根据请求判断访问登录页面,还是在登录页面中发送登录请求.
  • get->访问登录页面
  • post->登录请求

Shiro授权

获取授权信息

  • 在自定义的Realm中重写doGetAuthorizationInfo()方法,获取数据库中的权限信息,根据对应的权限为当前用户授权
    • 判断是否管理员,获得全部权限
    • 根据当前用户的id查询所有对应的角色编号
    • 查询当前用户的所有权限表达式
    • 封装到AuthorizationInfo对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//获取授权信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取当前登录用户的角色和权限,封装到AuthorizationInfo对象中并返回
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

//如果当前用户是admin管理员,获得所有权限 *:*
Employee currentEmp = (Employee) principalCollection.getPrimaryPrincipal();
if (currentEmp.isAdmin()) {
info.addRole("admin");
info.addStringPermission("*:*");
}

//如果不是超级管理员,则去数据库查询当前用户的权限,然后授权
//查询当前用户的信息(sn)编号
List<String> sns = roleMapper.selectSNByEmpid(currentEmp.getId());
//查询当前用户的权限信息
List<String> expressions = permissionMapper.selectExpressionByEmpId(currentEmp.getId());

info.addRoles(sns);
info.addStringPermissions(expressions);

return info;
}

使用Shiro提供的权限注解

Controller的方法上贴上Shiro提供的权限注解(@RequiresPermissions)

  • logical属性:
    • Logical.AND:必须存在多个权限表达式
    • Logical.OR:只拥有一个权限表达式即可
  • value属:
    • 同时设置权限表达式和权限的名称
1
@RequiresPermissions(value = {"部门列表","department:list"}, logical = Logical.OR)

在shiro.xml配置权限注解扫描器

1
2
3
4
<!--权限注解扫描器-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

当扫描到Controller中有使用@RequiresPermissions注解时,会使用动态代理为当前Controller生成代理对象,增强对应方法的权限校验功能.

生成权限信息到数据库

  • 在PermissionServiceImpl实现类中
    • 扫描Controller类中的方法
    • 生成权限信息到数据库中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void reload() {

List<String> expressions = permissionMapper.listAllExpressions();

//2.从容器中获取所有贴注解的控制器对象
Map<String, Object> beansWithAnnotation = ctx.getBeansWithAnnotation(Controller.class);
Collection<Object> collections = beansWithAnnotation.values();
//3.从每个控制器获取方法
for (Object ele : collections) {
//获取controller对象的父类方法
Method[] methods = ele.getClass().getSuperclass().getDeclaredMethods();

//4.获取方法上的注解
for (Method method : methods) {
if (method.isAnnotationPresent(RequiresPermissions.class)) {
RequiresPermissions annotation = method.getAnnotation(RequiresPermissions.class);
String[] values = annotation.value();
//5.获取注解中传递的参数
String name = values[0];
String expression = values[1];

if (!expressions.contains(expression)) {
//6.将参数保存到数据库中
Permission permission = new Permission();
permission.setName(name);
permission.setExpression(expression);

permissionMapper.insert(permission);
}
}
}
}

}

权限异常处理

访问了没有权限的页面会抛出UnauthorizedException异常

使用Spring MVC的统一异常处理,在mvc.xml中配置异常页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--配置异常页面-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定义默认的异常处理页面,当该异常类型的注册时使用 -->
<property name="defaultErrorView" value="common/error"/>
<!-- 定义异常处理页面用来获取异常信息的变量名,默认名为exception -->
<property name="exceptionAttribute" value="ex"/>
<!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常也页名作为值 -->
<property name="exceptionMappings">
<!-- 这里还可以继续扩展不同异常类型的异常处理 -->
<value>
org.apache.shiro.authz.UnauthorizedException=common/nopermission
</value>
</property>
</bean>

异步请求没有权限异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ControllerAdvice
public class UnauthorizedExceptionUtil {
@ExceptionHandler(UnauthorizedException.class)
public void handler(HttpServletResponse response, HandlerMethod
method,UnauthorizedException e) throws IOException {
if(method.getMethod().isAnnotationPresent(ResponseBody.class)){
response.setContentType("text/json;charset=UTF-8");
JSONResult result = new JSONResult();
result.mark("对不起,您没有权限执行该操作");
response.getWriter().print(JSON.toJSONString(result));
}else{
throw e;
}
}
}

Shiro标签

在前端页面使用shiro标签,从而更加细致的显示用户对应的权限,没有权限的相关操作隐藏.

引入jar包

1
2
3
4
5
<dependency>
<groupId>net.mingsoft</groupId>
<artifactId>shiro-freemarker-tags</artifactId>
<version>1.0.0</version>
</dependency>

新建CRMFreeMarkerConfigurer类,在FreeMark中注册shiro标签

重写afterPropertiesSet()方法

1
2
3
4
5
6
7
8
@Override
public void afterPropertiesSet() throws IOException, TemplateException {
super.afterPropertiesSet();

Configuration cfg = this.getConfiguration();
//创建标签头为shiro的shiro标签
cfg.setSharedVariable("shiro",new ShiroTags());
}

在mvc.xml中将MyFreeMarkerCong设置成当前环境中使用的配置对象

1
2
3
4
5
6
7
<!--配置freeMarker的模板路径 -->
<bean class="me.cscar.rbac.shiro.CRMFreeMarkerConfigurer">
<!-- 配置freemarker的文件编码 -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 配置freemarker寻找模板的路径 -->
<property name="templateLoaderPath" value="/WEB-INF/views/"/>
</bean>

shiro的freemarker常用标签

标签 作用
<@shiro.authenticated>… 已认证通过的用户
<@shiro.notAuthenticated>… 未认证通过的用户
<@shiro.principal property=”name” /> 输出当前用户信息,通常为登录帐号信息
<@shiro.hasRole name=”admin”>… 验证当前用户是否属于该角色
<@shiro.hasAnyRoles name=”admin,user,operator”>… 验证当前用户是否属于这些角色中的任何一个
<@shiro.hasPermission name=”/order:*”>… 当前用户是否拥有该权限

MD5加密

在添加用户的时候,需要对添加的用户密码进行加密

在EmployeeServiceImpl中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void insertOrUpdate(Employee record, Long[] roleIds) {
//使用当前用户名为密码加盐
record.setPassword(new Md5Hash(record.getPassword(), record.getName()).toString());
if (record.getId() == null) {
employeeMapper.insert(record);

} else {
employeeMapper.updateByPrimaryKey(record);
employeeMapper.deleteEmpAndRoleRelation(record.getId());
}
if (roleIds != null) {
for (Long roleId : roleIds) {
employeeMapper.insertEmpAndRoleRelation(record.getId(), roleId);
}
}
}

在CRMRealm中,认证的时候,密码匹配需要用到的密码应该和添加用户时的加密规则一致

1
2
3
4
5
6
7
8
9
if (currentEmp != null) {
//currentEmp:作为身份对象使用,因为在后面需要使用到当前登录用户的信息
return new SimpleAuthenticationInfo(
currentEmp,//身份信息
currentEmp.getPassword(),//凭证
ByteSource.Util.bytes(currentEmp.getName()),//盐
this.getName()//Realm名称
);
}

在shiro.xml配置需要的加密算法

1
2
3
4
5
6
7
<!--指定当前需要使用的凭证适配器-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--指定加密算法-->
<property name="hashAlgorithmName" value="MD5"/>
<!--指定加密次数-->
<!--<property name="hashIterations" value="3"/>-->
</bean>

在Realm中,将容器中的配置的凭证匹配器注入给当前的Realm对象
在该Realm中使用指定的凭证匹配器来完成密码匹配的操作

1
2
3
4
5
6
//注入给当前的Realm对象(@Autowired还可以注入set方法)
@Autowired
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(credentialsMatcher);
}

EhCache缓存

用户登陆后,授权信息一般很少变动,所有我们可以在第一次授权后就把这些授权信息存到缓存中,下一次就直接从缓存中获取,避免频繁访问数据库.

使用EhCache实现缓存

添加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.8</version>
</dependency>

在shiro.xml中

1
2
3
4
<!--配置缓存管理器并引用缓存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>

在上面的安全管理器中注入

1
2
3
4
5
6
<bean id="securityManager"
class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="CrmRealm"/>
<!--将缓存管理器注入到安全管理器-->
<property name="cacheManager" ref="cacheManager"/>
</bean>

添加ehcache配置文件:shiro-ehcache.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">

<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
属性 作用
maxElementsInMemory 缓存最大个数
eternal 对象是否永久有效
timeToIdleSeconds 设置对象在失效前的允许闲置时间(单位:秒)
timeToLiveSeconds 设置对象在失效前允许存活时间(单位:秒)
overowToDisk 当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中.LRU(最近最少使用)
  • 清空缓存
    • 如果用户正常退出,缓存自动清空.
    • 如果用户非正常退出,缓存自动清空.
    • 如果修改了用户的权限,而用户不退出系统,修改的权限无法立即生效.
    • 当用户权限修改后,用户再次登陆shiro会自动调用realm从数据库获取权限数据,如果在修改权限后想立即清除缓存则可以调用realm的clearCache方法清除缓存

在realm中定义该方法

1
2
3
4
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}

最后在角色或权限service中,delete或者update方法去调用realm的清除缓存方法.