Shiro基本知识-身份认证-权限验证
什么是Shiro?
Shiro是apache旗下一个开源安全框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证、权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
官方网站:http://shiro.apache.org/index.html
Shiro的功能模块如下图所示:
-
Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
-
SessionManagement:对用户的会话信息进行管理,使Session不再仅限于JavaEE应用,同时扩展了Session数据的存储途径及缓存方式,更易于实现Session数据的集群共享。
-
Cryptography:加密,保护数据的安全性,以简洁的API提供常用的加密算法和数据摘要算法。
为什么使用Shiro?
-
使用shiro可以非常快速的完成认证、授权等功能的开发,降低系统成本。
-
较之Spring Security,Shiro在保持强大功能的同时,在简单性和灵活性方面拥有较为明显的优势。
什么时候使用Shiro?
- 在项目中需要实现身份验证、权限授权等功能时,都可以使用Shiro来实现。
Shiro架构设计
Shiro架构设计如下图所示:
通过Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括:
认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象,
以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问)
- Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
- SecurityManager(安全管理器):Shiro的核心,用来协调管理组件工作。
- Authenticator(认证管理器):负责执行认证操作。
- Authorizer(授权管理器):负责授权检测。
- SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的Session 体验。
- SessionDAO:代表SessionManager执行Session持久(CRUD)操作,它允许任何存储的数据挂接到session管理器上。
- CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
- Cryptography(加密管理器):提供了加密方式的设计及管理。
- Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。
模拟面试题:Shiro内部是如何实现身份认证的?
答:
- 应用使用Subject向Shiro提交身份验证信息(用户名+密码/或其他)。
- 身份验证信息被提交给SecurityManager,SecurityManager会调用Authenticator进行身份验证。
- Authenticator需要调用对应的Realm从系统指定的位置获取正确的身份信息(数据库中保存的正确的用户名+密码)。
- Authenticator负责将用户提交的身份验证信息和正确信息进行比对,并返回比对结果。
模拟面试题:Shiro内部是如何实现权限认证的?
基于Shiro实现身份验证
身份验证整体实现逻辑
持久层开发
Shiro在进行用户身份验证时,需要获取数据库中保存的用户密码密文及盐值,以进行用户名密码验证。
在SysUserMapper
接口中开发如下方法:
/**
* 基于用户名查询用户信息
* @param username 用户名
* @return 用户信息
*/
SysUserDO getUserByUsername(String username);
在SysUserMapper.xml
中配置如下映射:
<!-- 基于用户名查询用户信息 -->
<!-- SysUserDO getUserByUsername(String username) -->
<select id="getUserByUsername" resultType="cn.sd.db.sys.pojo.SysUserDO">
select
*
from
sys_users
where
username=#{username}
</select>
在SysUserMapperTests
中开发测试用例如下:
@Test
public void getUserByUsername() {
SysUserDO user=mapper.getUserByUsername("tom1");
System.err.println(user);
}
在之前开发的添加用户的业务层方法中,少了一行为用户设置默认启动的代码,需要添加到SysUserServiceImpl
的saveSysUser
方法中:
// 设置当前用户默认为启用状态
sysUserDO.setValid(1);
在项目中添加Shiro的依赖
在项目的pom.xml
中添加如下依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
开发Realm类
创建cn.sd.db.shiro.realm.ShiroUserRealm
,继承org.apache.shiro.realm.AuthorizingRealm
,并添加如下代码:
public class ShiroUserRealm extends AuthorizingRealm{
@Autowired
private SysUserMapper userMapper;
/**
* 设置凭证匹配器(与用户添加操作使用相同的加密算法)
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
//构建凭证匹配对象
HashedCredentialsMatcher cMatcher=
new HashedCredentialsMatcher();
//设置加密算法
cMatcher.setHashAlgorithmName("MD5");
//设置加密次数
cMatcher.setHashIterations(5);
super.setCredentialsMatcher(cMatcher);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//1. 获取用户名(用户页面输入)
UsernamePasswordToken upToken=(UsernamePasswordToken)token;
String username=upToken.getUsername();
//2. 基于用户名查询用户信息
SysUserDO user=userMapper.getUserByUsername(username);
//3. 判断用户是否存在
if(user==null) {
throw new UnknownAccountException();
}
//4.判断用户是否被禁用
if(user.getValid()==0) {
throw new LockedAccountException();
}
//5.封装用户信息
ByteSource credentialsSalt=ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(
user, // principal身份
user.getPassword(), // hashedCredentials 加密后的密码
credentialsSalt, // credentialsSalt 盐值
getName()); //realmName
//6.返回封装结果
// 返回值会传递给认证管理器
return info;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
// TODO Auto-generated method stub
return null;
}
}
开发Shiro配置类
创建cn.sd.db.shiro.config.ShiroConfig
,在类上添加@Configuration
标签,并添加如下方法:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 过滤器配置
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置退出过滤器,退出逻辑Shiro已经实现
filterChainDefinitionMap.put("/logout", "logout");
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/bower_components/**", "anon");
filterChainDefinitionMap.put("/build/**", "anon");
filterChainDefinitionMap.put("/dist/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
// 放行登录请求
filterChainDefinitionMap.put("/user/login", "anon");
// 除了上面的资源,其他资源都需要进行认证,这个要放在所有放行资源之后
filterChainDefinitionMap.put("/**", "authc");
// 设置登录页面的路径,需要认证时,自动跳转该url
shiroFilterFactoryBean.setLoginUrl("/login_page");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 将自己的验证方式加入容器
@Bean
public Realm myShiroRealm() {
ShiroUserRealm myShiroRealm=new ShiroUserRealm();
return myShiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
@Bean(name="lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
// 对Shiro的Bean的生命周期进行管理
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
控制器层开发
在SysUserController
中开发登录方法:
@PostMapping("/login")
public JsonResult<Void> login(String username,String password){
//1. 获取Subject对象
Subject subject=SecurityUtils.getSubject();
//2. 通过Subject提交用户信息,交给Shiro框架进行认证操作
//2.1 对用户信息进行封装
UsernamePasswordToken token=new UsernamePasswordToken(username, password);
//2.2 对用户信息进行身份认证
subject.login(token);
//3 返回登录结果
return new JsonResult<Void>(STATE_SUCCESS,MSG_SUCCESS);
}
在BaseController
中添加统一处理Shiro异常的方法:
@ExceptionHandler(ShiroException.class)
@ResponseBody
public JsonResult<Void> doHandleShiroException(ShiroException e) {
JsonResult<Void> r=new JsonResult<Void>();
r.setState(0);
// 注意:现实开发中,这里的错误提示信息要尽量模糊
// 不要给攻击者任何可以判断账户状态的信息
if(e instanceof UnknownAccountException) {
r.setMessage("账户不存在");
}else if(e instanceof LockedAccountException) {
r.setMessage("账户已被禁用");
}else if(e instanceof IncorrectCredentialsException) {
r.setMessage("密码不正确");
}else if(e instanceof AuthorizationException) {
r.setMessage("没有此操作权限");
}else {
r.setMessage("系统维护中");
}
e.printStackTrace();
return r;
}
在PageController
中添加如下方法,返回登录页面:
@RequestMapping("/login_page")
public String findloginPage(){
return "login";
}
同时,需要使PageController
继承BaseController
,以统一处理Shiro可能抛出的异常。
登录页面开发
在项目的static/pages/login.html
中添加如下js代码:
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%' // optional
});
$(".btn").click(doLogin);
});
function doLogin(){
var params={
username:$("#usernameId").val(),
password:$("#passwordId").val()
}
var url="user/login";
console.log("params",params);
$.post(url,params,function(result){
if(result.state==20){
alert("登录成功!");
location.href="/"; //自动跳转首页
}else{
// 显示错误提示信息
$(".login-box-msg").html(result.message);
}
return false;//防止刷新时重复提交
});
}
功能测试
使用主类启动项目,在浏览器地址栏访问localhost:8080
,Shiro过滤器生效,会自动跳转login.html
页面,用户登录成功后,会自动跳转项目首页。
基于Shiro实现权限验证
权限验证整体实现逻辑
持久层开发
Shiro进行权限验证时,需要获取当前用户的权限信息,获取流程是:userId -> roleIds -> menuIds -> permissions属性值。
通过用户id获取角色id
在SysUserMapper
中声明如下抽象方法:
/**
* 基于用户id查询所有关联的角色id
* @param userId 用户id
* @return 角色id的集合
*/
List<Integer> listRoleIdByUserId(Integer userId);
在SysUserMapper.xml
中配置如下映射:
<!-- 基于用户id查询所有关联的角色id -->
<!-- List<Integer> listRoleIdByUserId(Integer userId) -->
<select id="listRoleIdByUserId" resultType="java.lang.Integer">
select
role_id
from
sys_user_roles
where
user_id=#{userId}
</select>
测试用例如下:
@Test
public void listRoleIdByUserId() {
List<Integer> list=mapper.listRoleIdByUserId(17);
for(Integer roleId:list) {
System.err.println("roleId="+roleId);
}
}
通过角色id获取菜单id
在`SysRoleMapper`接口中声明如下方法:
/**
* 基于角色id查询所有的菜单id
* @param roleIds 角色id数组
* @return 菜单id的集合
*/
List<Integer> listMenuIdByRoleId(@Param("roleIds")Integer[] roleIds);
在SysRoleMapper.xml
中配置如下映射:
<!-- 基于角色id查询所有的菜单id -->
<!-- List<Integer> listMenuIdByRoleId(@Param("roleIds")Integer[] roleIds) -->
<select id="listMenuIdByRoleId" resultType="java.lang.Integer">
select
menu_id
from
sys_role_menus
where
role_id
in
<foreach collection="roleIds"
open="(" close=")"
separator="," item="roleId">
#{roleId}
</foreach>
</select>
测试用例:
@Test
public void listMenuIdByRoleId() {
Integer[] roleIds= {48,49};
List<Integer> list=mapper.listMenuIdByRoleId(roleIds);
for(Integer menuId:list) {
System.err.println("menuId="+menuId);
}
}
通过菜单id获取permissions属性值
在SysMenuMapper
接口中声明如下方法:
/**
* 基于菜单id查询权限信息
* @param menuIds 菜单id
* @return 权限信息数组
*/
List<String> listPermissions(@Param("menuIds")Integer[] menuIds);
在SysMenuMapper.xml
文件中配置如下映射:
<!-- 基于菜单id查询权限信息 -->
<!-- List<String> listPermissions(
@Param("menuIds")Integer[] menuIds) -->
<select id="listPermissions" resultType="java.lang.String">
select
permission
from
sys_menus
where
id
in
<foreach collection="menuIds"
open="(" close=")"
separator="," item="menuId">
#{menuId}
</foreach>
</select>
测试用例:
@Test
public void listPermissions() {
Integer[] menuIds= {8,45,46,47,48};
List<String> list=mapper.listPermissions(menuIds);
for(String permission:list) {
System.err.println("permission="+permission);
}
}
在Realm类中添加权限验证逻辑:
在ShiroUserRealm
的doGetAuthorizationInfo
方法中添加权限验证的逻辑:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1.获取登录用户信息,例如用户id
SysUserDO user=(SysUserDO)principals.getPrimaryPrincipal();
Integer userId=user.getId();
//2.基于用户id获取用户拥有的角色(sys_user_roles)
List<Integer> roleIds=userMapper.listRoleIdByUserId(userId);
if(roleIds==null||roleIds.size()==0) { // 用户未绑定角色
throw new AuthorizationException();
}
//3.基于角色id获取菜单id(sys_role_menus)
Integer[] array={};
List<Integer> menuIds=
roleMapper.listMenuIdByRoleId(roleIds.toArray(array));
if(menuIds==null||menuIds.size()==0) {// 未绑定菜单
throw new AuthorizationException();
}
//4.基于菜单id获取权限标识(sys_menus)
List<String> permissions=
menuMapper.listPermissions(menuIds.toArray(array));
//5.对权限标识信息进行封装并返回
Set<String> set=new HashSet<String>();
for(String per:permissions){
if(!StringUtils.isEmpty(per)){
set.add(per);
}
}
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(set);
return info;//返回给授权管理器
}
在业务层接口中添加权限配置
在各业务层接口方法上,根据方法所属的增删改查操作类型,使用@RequiresPermissions("sys:role:view")
注解为方法配置访问权限。
注意:访问权限应该与数据库sys_menus
表中permission
字段的值一致。
例如:
/**
* 查询所有角色的id和name
* @return
* @throws RecordNotFoundException
*/
@RequiresPermissions("sys:role:view")
List<SysRoleDO> findAllSysRole()
throws RecordNotFoundException;
功能测试
创建一个仅具备角色管理
相关权限的用户,登录后,分别访问角色管理
模块和用户管理
模块,查看访问是否收到限制。
Shiro和权限管理子系统的关系
-
权限管理子系统通过菜单模块列出了项目中所有的资源(权限),通过角色模块来为角色分配对应的菜单(操作权限),通过用户模块来为用户分配对应的角色,以此实现项目中每个用户的操作权限的管理。
-
Shiro是基于权限管理子系统提供的关联信息,实现用户操作的权限控制。