apache Shiro是一个强大且易用的Java安全框架,能够执行身份验证、授权、加密和会话管理。
# apache Shiro的主要API
## Subject
Subject即“当前操作用户”。但是,在Shiro中,Subject这一概念并不是"帐户",而是与shrio交互的当前应用。
## SecurityManager
SecurityManager是Shiro框架的核心,典Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
## Realm
Realm是安全数据源,其中包含认证和授权数据。
也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,
如果缺省的Realm不能满足需求,可以自定义Realm实现。
# 使用Apache Shiro
## 引入依赖
<!-- Shiro核心包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro Web支持包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro与Spring的集成包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
## 在web.xml配置Shiro的代理过滤器
说明:该过滤器仅仅是过滤器代理,其功能由spring中配置的shrio过滤器实现
<!-- 此shiro代理过滤器的名称“shiroFilter”应当和Spring配置文件中的Shiro过滤 bean名称相匹配一致 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- 确保所有的需要通过Shrio的请求被过滤 -->
<!-- 通常这个过滤器配置在所有过滤器之前 -->
<!-- 却表Shiro和随后的过滤器在一个过滤器中工作 -->
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
## 开发自定义Realm
/**
* 通过继承AuthorizingRealm实现自定义Realm。
*/
public class SaftyRealm extends AuthorizingRealm {
@Resource
private SaftyService saftyService;
/**
* 获取当事人(当前用户)授权信息 参数PrincipalCollection 当事人集合
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
/*
* 获取登录用户帐号
*/
CurrUserDto currUser = (CurrUserDto) principalCollection.getPrimaryPrincipal();// 获取首要(第一)当事人
/*
* 创建授权信息对象
*/
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//
// // 查询角色
// List<Role> roleList = saftyService.getRolesOfUser(currUser.getUserId());
//
// // 将角色信息放入授权信息对象
// for (Role role : roleList) {
// simpleAuthorizationInfo.addRole(String.valueOf(role.getRo_id()));
// }
//
/*
* 查询用户权限,并将权限放入授权信息对象中
*/
List<Module> moduleList = saftyService.getModulesOfUser(currUser.getUserId());
for (Module module : moduleList) {
simpleAuthorizationInfo.addStringPermission(String.valueOf(module.getM_id()));
}
// System.out.println(currUser.getUserId()+"->"+simpleAuthorizationInfo.getStringPermissions());
/*
* 返回授权信息
*/
return simpleAuthorizationInfo;
}
/**
* 获取认证信息 参数 AuthenticationToken 认证令牌(如:一组用户名和密码就是一个认证令牌)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String userId = (String) token.getPrincipal();// 获得当事人(当前用户)
User user = saftyService.getUser(userId);
/*
* 如果不存在前用户信息,返回null
*/
if (user == null) {
return null;
}
/*
* 创建当前用户
*/
CurrUserDto currUser = new CurrUserDto();
currUser.setUserId(user.getU_id());
currUser.setUserName(user.getU_name());
/*
* 创建认证信息,三个构造参数含义依次如下:
* 参数1:principal当前用户
* 参数2:credentials认证凭证(如:口令、密码等)
* 参数3:realm名称
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(currUser, user.getU_pwd(), this.getName());
/*
* 返回认证信息
*/
return info;
}
}
## 在spring配置文件中配置Shiro
<!-- 保证实现了Shiro内部lifecycle(回调)函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- 配置自定义 realm -->
<bean id="saftyRealm" class="com.tnr.scgcxx.shiro.SaftyRealm"/>
<!-- 配置自定义securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" >
<property name="realm" ref="saftyRealm" />
<property name="sessionMode" value="native"/><!-- 配置使用Shiro本地Session,该Session会与HttpSession数据同步 -->
</bean>
<!-- 配置Shiro过滤器,该过滤器名称应当与web.xml配置的代理过滤器名称一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/><!-- 必须设置 -->
<!-- 3 个 url 属性为可选设置 -->
<property name="loginUrl" value="/safty/login_to"/> <!-- 登录界面url -->
<property name="successUrl" value="/safty/home_to"/> <!-- 登录成功后界面url -->
<property name="unauthorizedUrl" value="/safty/unauthorized_to"/> <!-- 未授权界面url -->
<!--
定义过滤器链,即定义shiro的ini配置信息(拦截规则),说明如下:
/static/** = anon 以路径“/static/”开头的所有地址均可匿名访问,无需认证
/safty/login_to = anon 路径“/safty/login_to”可匿名访问,无需认证
/safty/login = anon 路径“/safty/login”可匿名访问,无需认证
/** = authc 所有路径需认证。注:此规则写在最后,表示之前规则生效的前提下,无法匹配之前规则情况下,若符合此规则则按此规则执行。
-->
<property name="filterChainDefinitions">
<value>
/static/** = anon
/safty/login_to = anon
/safty/login = anon
/** = authc
</value>
</property>
</bean>
## 控制器中的登录代码示例
@RequestMapping(value="/login",method=RequestMethod.POST)
public ResultDto login(@RequestBody UserDto userDto,HttpSession session) {
try {
Subject subject = SecurityUtils.getSubject();
//创建登录令牌
UsernamePasswordToken usernamePasswordToken =
new UsernamePasswordToken(userDto.getU_id(),userDto.getU_pwd());
//登录,该方法声明抛出异常,可以捕获异常,并进行应对处理
subject.login(usernamePasswordToken);
//是否通过认证
if(subject.isAuthenticated()) {
//获得当前用户信息
CurrUserDto currUser = (CurrUserDto)subject.getPrincipal();
//将当前用户放入Session。注:这里的Session是由Shiro提供的。
subject.getSession().setAttribute(Constants.SESSION_ATTR_NAME_CURR_USER, currUser);
return ResultDto.successResult();
}
return ResultDto.failResult("登录失败!");
} catch (UnknownAccountException e) {
return ResultDto.failResult("用户名不存在!");
} catch (IncorrectCredentialsException e) {
return ResultDto.failResult("账户密码 不正确!");
} catch (LockedAccountException e) {
return ResultDto.failResult("用户名 被锁定 !");
}catch (Exception e) {
e.printStackTrace();
return ResultDto.failResult("系统错误!");
}
}
## 控制器中的退出代码示例
@DeleteMapping("/logout")
public ResultDto login_authentication() {
try {
//注销
SecurityUtils.getSubject().logout();
return ResultDto.successResult();
} catch (Exception e) {
return ResultDto.failResult("退出失败!");
}
}
## 动态权限控制
上述spring中ShiroFilter配置拦截规则如下:
<property name="filterChainDefinitions">
<value>
/static/** = anon
/safty/login_to = anon
/safty/login = anon
/** = authc
</value>
</property>
显然,这些拦截规则仅包含认证部分,缺少授权部分,授权配置形式如:/docs/** = authc, perms["1010","2005"] 表示需要经过认证和xxx权限。
而系统中的权限往往是动态的,随用户不同而不同,所以这种静态配置拦截规则的方式就不合适了。
在这样的动态权限需求背景下,可以使用ShiroFilter另外一个Map类型属性filterChainDefinitionMap配置拦截规则,
而filterChainDefinitionMap可以通过一个自定义的FactoryBean获得。
### 动态权限控制示例
/**
* 自定义生成filterChainDefinitionMap的工厂bean
*/
public class ShiroDefinitionSectionFactory implements FactoryBean<Ini.Section> {
private static final Logger LOG = LogManager.getLogger(ShiroDefinitionSectionFactory.class);
public static final String PREMISSION_FORMAT = "authc,perms[{0}]";
@Resource
private SecurityManager securityManager;
@Resource
private SaftyService saftyService;
/**
* 注入先于动态权限加载的默认认证授权定义
*/
private String preFilterChainDefinitions;
/**
* 注入后于动态权限加载的默认认证授权定义
*/
private String postFilterChainDefinitions;
@Override
public Ini.Section getObject() throws Exception {
/*
* Ini是Map<String,Ini.Section>的实现类
* Ini.Section是Map<String,String>的实现类,表示一个Ini配置片段实例
*/
Ini ini = new Ini();
//加载动态权限前的ini配置
ini.load(preFilterChainDefinitions);
//获取ini配置片段
Ini.Section section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
/*
* 加载动态权限
*/
List<Module> moduleList = saftyService.getAllSubModules();
//由注入的资源管理对象获取所有资源数据,并且资源的authorities的属性是EAGER的fetch类型
for(Module module:moduleList) {
if(StringUtils.isEmpty(module.getM_url())) {
continue;
}
//将动态权限放入ini配置片段中
section.put(module.getM_url(), MessageFormat.format(PREMISSION_FORMAT, String.valueOf(module.getM_id())) );
}
//加载动态权限之后的ini配置
Ini postIni = new Ini();
postIni.load(postFilterChainDefinitions);
Ini.Section postSection=postIni.getSection(Ini.DEFAULT_SECTION_NAME);
//将动态权限之后的ini配置放入统一的ini配置片段中
section.putAll(postSection);
LOG.debug("=====Shiro安全规则=======================================================================");
LOG.debug(section.entrySet());
LOG.debug("=====Shiro安全规则=======================================================================");
return section;
}
public void setPreFilterChainDefinitions(String preFilterChainDefinitions) {
this.preFilterChainDefinitions = preFilterChainDefinitions;
}
public void setPostFilterChainDefinitions(String postFilterChainDefinitions) {
this.postFilterChainDefinitions = postFilterChainDefinitions;
}
@Override
public Class<?> getObjectType() {
return this.getClass();
}
@Override
public boolean isSingleton() {
return false;
}
}
修改spring配置文件
<!-- 配置自定义的生成filterChainDefinitionMap的工厂bean -->
<bean id="filterChainDefinitionMap" class="com.tnr.scgcxx.shiro.ShiroDefinitionSectionFactory">
<property name="preFilterChainDefinitions">
<value>
/static/** = anon
/safty/login_to = anon
/safty/login = anon
</value>
</property>
<property name="postFilterChainDefinitions">
<value>
/** = authc
</value>
</property>
</bean>
<!-- 配置Shiro过滤器,该过滤器名称应当与web.xml配置的代理过滤器名称一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/><!-- 必须设置 -->
<!-- 3 个 url 属性为可选设置 -->
<property name="loginUrl" value="/safty/login_to"/> <!-- 登录界面url -->
<property name="successUrl" value="/safty/home_to"/> <!-- 登录成功后界面url -->
<property name="unauthorizedUrl" value="/safty/unauthorized_to"/> <!-- 未授权界面url -->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>
</bean>