Shiro权限控制(一):Spring整合Shiro

一、目标:
1.介绍如何在SpringMVC中整合Shiro权限框架
2.介绍如何使用Shrio进行身份验证,如常见的登录
3.介绍如何控制哪些服务登录后才能访问,哪些服务不需要登录就可以访问

二、前言
本文是在前两篇博文《Spring+Spring MVC+Mybatis+Maven搭建多模块项目(一)》《Spring+Spring MVC+Mybatis+Maven搭建多模块项目(二)》的基础上整合Shiro框架,如果想了解Spring+SpringMVC+Mytatis是如何整合的,请查看前面的文章。

在项目中,我们经常会用到权限验证,在项目初期,架构师已经提前把权限框架整合到工程中,开发人员只需要按一定的规则进行开发即可,并不需要过多关心权限是怎么实现的,那Shiro到底是怎么实现权限控制的呢?

三、Shiro简单介绍
Shiro是Apache 旗下的一个简单易用的权限框架,可以轻松的完成 认证、授权、加密、会话管理、与 Web 集成、缓存等,这里只进行简单的介绍,详细的介绍请查阅官方文档,先来看下Shiro如何工作的
在这里插入图片描述
可以看到:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject;其每个 API 的含义:

Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager

SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;它是 Shiro 的核心,它负责与他组件进行交互,可以把它看成 DispatcherServlet 前端控制器

Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源

记住一点,Shiro 不会去维护用户、权限;需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

四、Shiro与Spring集成
1.在pom.xml文件中引入对Shiro的依赖

    <!-- shiro 包-->
	<dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-quartz</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.2</version>
    </dependency>

2.在config目录下创建Shiro配置文件spring-shiro-web.xml,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:aop="http://www.springframework.org/schema/aop" 
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
			http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
			http://www.springframework.org/schema/mvc 
    		http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd 
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/aop 
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
           http://www.springframework.org/schema/tx 
           http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    	<property name="realm" ref="userShiroRealm"/>
    </bean>
    
    <!-- 自定义域 -->
    <bean id="userShiroRealm" class="com.bug.realm.UserShiroRealm">
    	<property name="userService" ref="userService"/>
    	<property name="credentialsMatcher" ref="credentialsMatcher"/>
    	<property name="cachingEnabled" value="true"/>
    </bean>
    
     <!-- 自定义凭证(密码)匹配器 -->
    <bean id="credentialsMatcher" class="com.bug.credentials.BugCredentialsMatcher"></bean>
    
    <!-- 自定义登录验证过滤器 -->
    <bean id="loginCheckPermissionFilter" class="com.bug.filter.LoginCheckPermissionFilter"></bean>
    
    <!-- Shiro的web过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    	<property name="securityManager" ref="securityManager"></property>
    	<property name="unauthorizedUrl" value="/index.jsp"></property>
    	<property name="filters">
    		<map>
    			<entry key="authc" value-ref="loginCheckPermissionFilter"></entry>
    		</map>
    	</property>
    	<property name="filterChainDefinitions">
    		<value>
    			/index.jsp = anon
                /unauthorized.jsp = anon
                /user/checkLogin = anon
                /user/queryUserInfo = authc
                /user/logout = anon
                /pubApi/** = anon
    		</value>
    	</property>
    </bean>
    
</beans>

说明:
1.首先声明SecurityManager,用于管理所有的 Subject,在SecurityManager中需要引用Realm,也就是权限数据的来源,通过自定义的Realm告诉SecurityManager有哪些数据权限需要管理

2.自定义Realm,注入UserService,通过UserService获得用户信息,如用户名及密码,另外还自定义了凭证(密码)匹配规则credentialsMatcher

3.自定义登录验证过滤器loginCheckPermissionFilter,登录校验的核心过滤器,哪些服务需要登录才能访问,就通过此过滤器控制

4.声明Shiro的WEB过滤器,必须引入securityManager,否则启动报错,WEB过滤器主要用于URL的访问控制,控制哪些URL需要权限控制,哪些不需要权限控制,如
/user/logout = anon:表示不需要权限控制
/user/queryUserInfo = authc:表示需要登录后才能访问
/pubApi/** = anon:表示URL路径中存在pubApi关键字的,都不需要权限控制

5.配置文件中用到的userShiroRealm,credentialsMatcher及loginCheckPermissionFilter在下面会给出代码的实现

3.修改web.xml文件,将Shiro集成到工程中

3.1将上面增加的spring-shiro-web.xml文件引入到web.xml文件中

<!-- Spring配置 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:config/applicationContext.xml,
			classpath:config/spring-shiro-web.xml
		</param-value>
	</context-param>

3.2 在web.xml中配置shiroFilter过滤器,注意过滤器名称shiroFilter一定要和spring-shiro-web.xml中声明的shiroFilter保存一致

    <!-- shiro filter start -->
	<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>
	<filter-mapping>
	  <filter-name>shiroFilter</filter-name>
	  <url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- shiro filter end -->

4.自定义Realm
在上面的配置中,我们注入了自定义的Realm UserShiroRealm,此Realm需要继承AuthorizingRealm,并实现doGetAuthorizationInfo权限验证方法和doGetAuthenticationInfo身份验证方法,,代码实现如下

package com.bug.realm;

import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import com.bug.excption.BugException;
import com.bug.model.user.UserVO;
import com.bug.service.user.IUserService;
/**
 * 自定义Realm
 * @author longwentao
 *
 */
public class UserShiroRealm extends AuthorizingRealm{
	
	private IUserService userService;
	
	public void setUserService(IUserService userService) {
		this.userService = userService;
	}

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String username = (String)principals.getPrimaryPrincipal();
		if(username == null) {
			throw new BugException("未登录");
		}
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		Set<String> roles = new HashSet<String>();
		Set<String> stringPermissions = new HashSet<String>();
		roles.add("USER");
		stringPermissions.add("USER:DELETE");//角色:权限
		
		info.setRoles(roles);//角色可以通过数据库查询得到
		info.setStringPermissions(stringPermissions);//权限可以通过数据库查询得到
		
		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken autToken) throws AuthenticationException {

		UsernamePasswordToken userPwdToken = (UsernamePasswordToken) autToken;
		String userName = userPwdToken.getUsername();

		UserVO user = userService.selectUserByUserName(userName);
		if (null == user) {
			throw new BugException("未知账号");
		}
		
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(),
				user.getPassword().toCharArray(), getName());

		return authenticationInfo;
	}
}

说明:
1.Shiro进行身份验证时,会调用到doGetAuthenticationInfo方法,在方法内部,我们通过UsernamePasswordToken 获得用户传过来的用户名,再通过userService.selectUserByUserName方法从数据库中查询用户信息,如果用户为空,说账号不存在,否则将查询出来的用户名及密码,封装到SimpleAuthenticationInfo 对象中,并返回,用于接下来的密码验证

2.Shiro角色权限验证,会调用doGetAuthorizationInfo方法,通过SimpleAuthorizationInfo.setRoles()方法设置用户角色,通过SimpleAuthorizationInfo.setStringPermissions()设置用户权限,这里暂时给个空集合,在项目中,用户的角色权限需要从数据库中查询

5.自定义凭证(密码)匹配器
此过滤器主要用于凭证(密码)匹配,即校验用户输入的密码和从数据库中查询的密码是否相同,相同则返回true,否则返回false,此匹配器继承了SimpleCredentialsMatcher,并重写doCredentialsMatch方法,代码如下

package com.bug.credentials;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.util.SimpleByteSource;

/**
 * 自定义凭证(密码)匹配器
 * @author longwentao
 *
 */
public class BugCredentialsMatcher extends SimpleCredentialsMatcher {

	@Override
	public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
		// 对前台传入的明文数据加密,根据自定义加密规则加密
		Object tokencredential = new SimpleByteSource((char[]) token.getCredentials());
		// 从数据库获取的加密数据
		Object accunt = new SimpleByteSource((char[]) info.getCredentials());
		// 返回对比结果
		return equals(accunt, tokencredential);
	}
}

6.自定义登录验证过滤器
此过滤器主要用于校验用户访问某个URL时,是否已经提前登录过,如果登录过,则允许访问,否则拒绝访问;此过滤器继承了AuthorizationFilter,并重写了isAccessAllowed方法和onAccessDenied方法,代码如下

package com.bug.filter;

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定义登录验证过滤器
 * @author longwentao
 *
 */
public class LoginCheckPermissionFilter extends AuthorizationFilter {
	private final static Logger logger = LoggerFactory.getLogger(LoginCheckPermissionFilter.class);

	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object arg2) throws Exception {
		HttpServletRequest req = (HttpServletRequest) request;
		String url = req.getRequestURI();
		try {
			Subject subject = SecurityUtils.getSubject();

			return subject.isPermitted(url);
		} catch (Exception e) {
			logger.error("Check perssion error", e);
		}
		return false;
	}

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
		Subject subject = getSubject(request, response);
		if (subject.getPrincipal() == null) {
			saveRequestAndRedirectToLogin(request, response);
		} else {
			return true;
		}
		return false;
	}
}

说明:
在onAccessDenied方法中,如果用户身份为空,说明未登录,则跳转到登录页面,如果未指定跳转的路径,Shiro给了默认值的跳转页面 /login.jsp
在这里插入图片描述
到此,所有的配置及自定义的过滤器都已经实现完成,Shiro已经集成到项目中,接下来进行用例验证

五.验证准备工作

一个login.jsp页面及一个UserController.java,在Controller中提供3个服务,权限如下

  1. 一个登录页面login.jsp --不需要权限控制
  2. 登录校验服务 /user/checkLogin --不需要权限控制,即/user/checkLogin = anon
  3. 退出服务/user/logout --不需要权限控制,即/user/logout = anon
  4. 查询用户信息服务/user/queryUserInfo --需要登录后才可访问,即/user/queryUserInfo = authc

这里有个疑惑,退出服务为什么不要权限控制呢,如果A用户已经登录,那B用户知道退出的服务地址,直接请求退出服务,岂不是将A用户强制退出了?不着急,看下面的验证就知道会不会有影响了

login.jsp页面代码如下:

<form action="/bug.web/user/checkLogin" method="post">
    用户名:<input type="text" name="userName"><br/>
    密码:<input type="password" name="password"><br/>
    <input type="submit" value="登录">
</form>

UserController.java代码如下:

package com.bug.controller.user;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.bug.excption.BugException;
import com.bug.model.common.ResponseVO;
import com.bug.model.user.UserVO;
import com.bug.service.user.IUserService;

@Controller
@RequestMapping("/user")
public class UserController {
	private final static Logger logger = LoggerFactory.getLogger(UserController.class);

	@Autowired
	private IUserService userService;
	
	@RequestMapping(value = "/checkLogin", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded")
	@ResponseBody
	public ResponseVO<String> checkLogin(@RequestParam("userName") String userName,
			@RequestParam("password") String password) {
		ResponseVO<String> response = new ResponseVO<String>();
		try {
			UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
			
			Subject subject = SecurityUtils.getSubject();

			subject.login(token);
		}catch (Exception e) {
			logger.error("Login Error:",e);
			response.setStatus(ResponseVO.failCode);
			Throwable ex = e.getCause();
			if(ex instanceof BugException) {
				if(ex.getMessage() != null) {
					response.setMessage(ex.getMessage());
				}
			}else if(e instanceof IncorrectCredentialsException) {
				response.setMessage("密码错误");
			}else {
				response.setMessage("登录失败");
			}
		}

		return response;
	}
	@RequestMapping(value = "/logout", method = RequestMethod.GET)
	public ResponseVO<String> logout(){
		ResponseVO<String> response = new ResponseVO<String>();
		Subject subject = SecurityUtils.getSubject();
		if(subject.isAuthenticated()) {
			subject.logout();
		}
		return response;
	}
	
	@RequestMapping(value="/queryUserInfo",method = RequestMethod.GET)
	@ResponseBody
	public ResponseVO<UserVO> queryUserInfo() {
		ResponseVO<UserVO> response = new ResponseVO<UserVO>();
		try {
			UserVO user = userService.selectUserById("1");
			response.setData(user);
		} catch (Exception e) {
			logger.error("queryUserInfo error:",e);
			response.setStatus(ResponseVO.failCode);
		}

		return response;
	}

}

六、验证场景:

1.输入错误密码,看Shiro如何进行凭证校验
访问localhost:8080/bug.web/login.jsp,输入用户名及错误密码,点击登录
在这里插入图片描述

开始访问到UserController中的checkLogin,在checkLogin中使用Shiro提供的Subject.login方法进行登录
在这里插入图片描述
接下来会访问到UserShiroRealm.doGetAuthenticationInfo方法,在方法中使用传进来的username通过UserService查询用户信息
在这里插入图片描述
用户名验证通过后,从源码中可以看出接下来进行密码验证,在AuthenticatingRealm.getAuthenticationInfo方法中
在这里插入图片描述
在assertCredentialsMatch方法中,获得的CredentialsMatcher就是我们自定义的BugCredentialsMatcher
在这里插入图片描述
继续往下执行,就到了我们的自定义凭证(密码)匹配器BugCredentialsMatcher
在这里插入图片描述
继续向下执行,就会抛IncorrectCredentialsException异常,说明密码错误
在这里插入图片描述
异常抛出后,在异常处理中捕获,并将提示信息返回给用户,整个登录校验过程就完成了
在这里插入图片描述
在这里插入图片描述
2.未登录时,访问queryUserInfo 服务,看能否访问
直接访问http://localhost:8080/bug.web/user/queryUserInfo,Shiro发现用户未登录,已经自动重定向到登录页面
在这里插入图片描述
在这里插入图片描述
3.登录后,访问queryUserInfo 服务,看能否访问
登录后直接访问http://localhost:8080/bug.web/user/queryUserInfo,发现调用链是这样的:LoginCheckPermissionFilter.isAccessAllowed–>UserShiroRealm.doGetAuthorizationInfo–>LoginCheckPermissionFilter.onAccessDenied,在onAccessDenied方法中返回true,说明已经登录,用户可以访问
在这里插入图片描述
在这里插入图片描述
4.A用户已经登录,B用户未登录直接请求退出服务,校验A用户是否有影响
A用户登录后,B用户直接访问http://localhost:8080/bug.web/user/logout服务,发现subject.isAuthenticated()为false,并不会用户subject.logout();,因此A用户已经登录,B用户未登录直接请求退出服务,A用户没有影响
在这里插入图片描述

到此为止,Spring集成Shiro权限框架基本已经完成,当然,这只是最基本的访问控制,更复杂的权限控制需要整合角色一起设计,即什么角色拥有查询权限,什么角色拥有增删改权限,这些到下一篇博文中再介绍!!!

  • 17
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值