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管理对象(领域对象:负责处理认证和授权领域的数据访问)

  1. Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
  2. SecurityManager(安全管理器):Shiro的核心,用来协调管理组件工作。
  3. Authenticator(认证管理器):负责执行认证操作。
  4. Authorizer(授权管理器):负责授权检测。
  5. SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的Session 体验。
  6. SessionDAO:代表SessionManager执行Session持久(CRUD)操作,它允许任何存储的数据挂接到session管理器上。
  7. CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
  8. Cryptography(加密管理器):提供了加密方式的设计及管理。
  9. Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。

模拟面试题:Shiro内部是如何实现身份认证的?
答:

  1. 应用使用Subject向Shiro提交身份验证信息(用户名+密码/或其他)。
  2. 身份验证信息被提交给SecurityManager,SecurityManager会调用Authenticator进行身份验证。
  3. Authenticator需要调用对应的Realm从系统指定的位置获取正确的身份信息(数据库中保存的正确的用户名+密码)。
  4. 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);
}

在之前开发的添加用户的业务层方法中,少了一行为用户设置默认启动的代码,需要添加到SysUserServiceImplsaveSysUser方法中:

// 设置当前用户默认为启用状态
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类中添加权限验证逻辑:

ShiroUserRealmdoGetAuthorizationInfo方法中添加权限验证的逻辑:

	@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和权限管理子系统的关系

  1. 权限管理子系统通过菜单模块列出了项目中所有的资源(权限),通过角色模块来为角色分配对应的菜单(操作权限),通过用户模块来为用户分配对应的角色,以此实现项目中每个用户的操作权限的管理。

  2. Shiro是基于权限管理子系统提供的关联信息,实现用户操作的权限控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值