SSM整合安全框架Shiro案例

我们做项目一般很难避开权限及验证的问题,这就需要一个完整的权限框架来支持项目。Shiro是一个轻量级的安全框架,能够处理我们的绝大部分权限问题,并且可以很轻松的集成到现有的业务系统中。关于Shiro就不介绍了,网上的文章很多。下面我们来主要看一下Shiro的具体配置及使用。

通过Maven引入Shiro

首先我们还是通过Maven来引入Shiro的核心jar包。

	<!-- shiro -->
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-core</artifactId>
		<version>1.4.0-RC2</version>
	</dependency>
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-spring</artifactId>
		<version>1.4.0-RC2</version>
	</dependency>
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-web</artifactId>
		<version>1.4.0-RC2</version>
	</dependency>
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-ehcache</artifactId>
		<version>1.4.0-RC2</version>
	</dependency>

通过xml配置Shiro

我们通过引入单独的xml来配置Shiro。在Web.xml中增加如下配置,引入了shiro-config.xml文件,并且新增了shiroFilter。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	version="3.0">
	<display-name>Archetype Created Web Application</display-name>
	<!-- Spring 和 Mybatis 的配置文件 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:shiro-config.xml
			classpath:spring-mybatis.xml;
		</param-value>
	</context-param>
	<!-- shiro -->
	<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>
        <!-- 这里省略了后面其他的配置 -->

注:这里我们将shiroFilter放在了所有Filter的最前面,来过滤所有的请求。

下面来看看shiro-config.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:aop="http://www.springframework.org/schema/aop"  
	xmlns:tx="http://www.springframework.org/schema/tx"  
	xmlns:context="http://www.springframework.org/schema/context"  
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd  
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd  
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<!-- 配置自定义Realm -->
	<bean id="authRealm" class="com.wte.config.shiro.AuthRealm" />

	<!-- 安全管理器 -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="authRealm" />
	</bean>

	<!-- Shiro过滤器 核心 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<!-- Shiro的核心安全接口,这个属性是必须的 -->
		<property name="securityManager" ref="securityManager" />
		<!-- 身份认证失败,则跳转到登录页面的配置 -->
		<property name="loginUrl" value="/login" />
		<!-- 权限认证失败,则跳转到指定页面 -->
		<property name="unauthorizedUrl" value="/story/mainpage.jsp" />
		<!-- Shiro连接约束配置,即过滤链的定义 -->
		<property name="filterChainDefinitions">
			<value>
				<!--anon 表示匿名访问,不需要认证以及授权 -->
				/=anon
				/ux/**=anon
				/login=anon
				/dologin=anon
				/pub/**=anon
				<!--自定义urlperm表示需要认证,并且对jsp的访问会检查权限 -->
				/story/**=urlperm
				<!--authc表示需要认证 没有进行身份认证是不能进行访问的 -->
				/**=authc
			</value>
		</property>
		<!-- 自定义的过滤器,来实现自己需要的授权过滤方式。 -->
		<property name="filters">
            <map>    
               <entry key="urlperm" value-ref="urlPermissionFilter"/>  
            </map>    
        </property>
	</bean>
 	<bean id="urlPermissionFilter" class="com.wte.config.filter.URLAuthenticationFilter" />

	<!-- 全局异常处理-->
	<bean id="exceptionResolver" class="com.wte.config.exception.GlobalExceptionResolver"/>
    </beans>

注:其中的注释写的已经比较清楚了。需要改动的就是我们验证失败跳转的地址以及认证授权的规则。
1. anon表示可以匿名访问,authc表示需要身份验证才可以访问。其中我们自己定义了一个urlperm规则,用来处理我们自定义的权限验证。这里我用它来检查访问的jsp是否有权限访问,可以看到这里配置了一个过滤器。当然如果不需要,完全可以删掉这个自定义的配置,后面的过滤器。
2. 这里包含了全局异常的处理。异常处理有三种方式,详细可以参考本文后面的参考资料。

来看看这个过滤器的实现,这里是我实际的处理,如果上面没有配置urlperm,则此处的过滤器可以忽略。:

public class URLAuthenticationFilter implements Filter { 
	@Autowired
	IMenuService menuService;
	
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { 
        HttpServletRequest httpRequest = (HttpServletRequest)request; 
        HttpServletResponse httpResponse = (HttpServletResponse)response; 
        
        //检查是否登录
        if(SecurityUtils.getSubject().isAuthenticated()){
             //检查jsp页面是否有访问权限
            List<MenuBean> menuList= menuService.findMenuAll();
            String url=httpRequest.getRequestURI().replace(httpRequest.getContextPath(), ""); 
            if(url.endsWith(".jsp") ){
            	if(!checkIsPermitted(menuList,url)){
            		try{
                		httpResponse.sendRedirect(httpRequest.getContextPath() + "/story/pub/unauthorized.jsp"); 
                		return ;
        	        }catch(Exception ex){
        	        }
            	}
            }
        }else{
        	try{
        		httpResponse.sendRedirect(httpRequest.getContextPath() + "/login"); 
        		return ;
	        }catch(Exception ex){
	        }
        }
        try { 
        	chain.doFilter(request,response); 
        }catch(Exception ex){
        	
        }
    } 
    
    /**
     * 检查请求地址是否有权限
     * @param list
     * @param url
     * @return
     */
    private boolean checkIsPermitted(List<MenuBean> list,String url){
    	Subject subject= SecurityUtils.getSubject();
    	for(MenuBean entity:list){
    		if(entity.getPath().equals(url)){
    			if(!subject.isPermitted(entity.getFuncCode())){
    				return false;
    			}else{
    				return true;
    			}
    		}
    	}
    	return true;
    }
        
    public void init(FilterConfig filterConfig) throws ServletException { 
    	// TODO Auto-generated method stub 
    } 
    public void destroy() { 
        // TODO Auto-generated method stub 
    }
}

再来看看我们定义的全局异常处理:

public class GlobalExceptionResolver implements HandlerExceptionResolver {
	private static final Logger log = Logger.getLogger(GlobalExceptionResolver.class);// 日志文件

	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
		if((ex instanceof AuthenticationException) || (ex instanceof UnauthorizedException)){
        	//未授权401
        	response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }
		
		if(isAjaxRequestInternal(request, response)){
			ModelAndView mv = new ModelAndView();
            /*使用FastJson提供的FastJsonJsonView视图返回,不需要捕获异常   */
            FastJsonJsonView view = new FastJsonJsonView();
            Map<String, Object> attributes = new HashMap<String, Object>();
            attributes.put("code", "1000001");
            attributes.put("msg", ex.getMessage());
            view.setAttributesMap(attributes);
            mv.setView(view);
            log.debug("异常:" + ex.getMessage(), ex);
            return mv;
		}else{
			Map<String, Object> model = new ConcurrentHashMap<String, Object>();
			model.put("ex", ex);
			// 可以细化异常信息,给出相应的提示
			log.info("==========发生了异常:");
			log.info("==========异常类型:" + ex.getClass().getSimpleName());
			log.info("==========异常描述:" + ex.getMessage());
			log.info("==========异常原因:" + ex.getCause());
			return new ModelAndView("pub/error", model);
		}
	}

	/**
	 * 判断Request是否为Ajax请求
	 * @param request
	 * @param response
	 * @return
	 */
	private boolean isAjaxRequestInternal(HttpServletRequest request, HttpServletResponse response) {
		return (request.getHeader("X-Requested-With") != null &&"XMLHttpRequest".equals( request.getHeader("X-Requested-With").toString()) ) ;
	}
}

增加AuthRealm类

我们需要自定义AuthRealm类,并且继承自AuthorizingRealm抽象类,这其中有两个方法。

public class AuthRealm extends AuthorizingRealm {
    @Resource
    private IAuthLocalService authLocalService;

    /**
     * 用于权限的认证。
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = UserContext.getCurrentUser().getUserName();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo() ;
        Set<String> roleName = authLocalService.findRoles(username) ;
        Set<String> permissions = authLocalService.findPermissions(username) ;
        info.setRoles(roleName);
        info.setStringPermissions(permissions);
        return info;
    }

    /**
     * 首先执行这个登录验证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //获取用户账号
        String username = token.getPrincipal().toString() ;
        String password = new String((char[]) token.getCredentials());
        AuthLocalBean user = authLocalService.findUserByUsername(username) ;
        
        if(user == null) {
            throw new UnknownAccountException("account is not exist.");
        }else if(!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("account or password is not correct.");
        }

        LoginUser loginUser= new LoginUser(user.getUserId(),user.getUsername());
        
        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginUser,user.getPassword(),getName()) ;
        return authenticationInfo ;
    }
}

注:这里的代码都比较简单,通过字面都能看懂。IAuthLocalService提供了系统中获取角色,权限等的一些操作,与我们自己实际的权限系统设计相关。来看下IAuthLocalService的定义:

public interface IAuthLocalService extends IBaseService<AuthLocalBean,AuthLocalDao>{
	public OperationResult<?> login(AuthLocalBean localAuthBean,HttpServletRequest request,HttpServletResponse response);
	public OperationResult<?> logout(HttpServletRequest request,HttpServletResponse response);

	/**
	 * 检查用户名是否重复
	 * @param userName
	 * @param userId
	 * @return
	 */
	public int queryCountByUserNameNotContaintUserId(String userName,Integer userId);
	
	public Set<String> findRoles(String userName);
	
	public Set<String> findPermissions(String userName);
	
	public AuthLocalBean findUserByUsername(String userName);
}

好了,到这里我们整个Shiro就已经被集成进来了,我们后面可以使用它给我们提供的功能来进行验证授权。

登录

我们具体的登录实现如下:

	/**
	 * 登录
	 */
	public OperationResult<?> login(AuthLocalBean localAuthBean, HttpServletRequest request,
			HttpServletResponse response) {
		try {
			HttpSession session = request.getSession();
			String code = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
			if(localAuthBean.getCaptchaCode().equals(code)){
				Subject subject = SecurityUtils.getSubject();
				String password = MD5Utils.MD5Encode(localAuthBean.getPassword());
				UsernamePasswordToken token = new UsernamePasswordToken(localAuthBean.getUsername(), password);
				subject.login(token);
			}else{
				//验证码错误
				return new OperationResult<LoginResultType>(OperationResultType.ValidError,LoginResultType.CaptchaCodeError);
			}
		} catch (UnknownAccountException e) {
			//账号不存在
			return new OperationResult<LoginResultType>(OperationResultType.ValidError,LoginResultType.UnknownAccount);
		} catch (IncorrectCredentialsException e) {
			//账号或密码错误
			return new OperationResult<LoginResultType>(OperationResultType.ValidError,LoginResultType.IncorrectCredentials);
		} catch (LockedAccountException e) {
			//账号被锁定
			return new OperationResult<LoginResultType>(OperationResultType.ValidError,LoginResultType.LockedAccount);
		} catch (AuthenticationException e) {
			//其他错误
			return new OperationResult<LoginResultType>(OperationResultType.Error,LoginResultType.Authentication);
		}
		return new OperationResult<LoginResultType>(OperationResultType.Success,LoginResultType.Success);
	}

注:这里的subject.login(token);会进入到我们AuthRealmdoGetAuthenticationInfo()方法来认证。其他的OperationResult等都是我们对返回结果的一个封装,没有什么其他的逻辑,所以看到这里除了检查验证码以外,登录的验证还是很简单的。

Shiro标签的使用

还记得我们上一篇中实现的权限功能吗?这次我们还是用它定义的权限来进行验证。我们还是以UserController为例:

@Controller
@RequestMapping("/sysmgr/user")
@Function(func="sysmgr.user",describe="用户管理")
public class UserController extends BaseController {
	@Resource
	private IUserService userService;
	@Resource
	private IAuthLocalService authLocalService;
	
	private static final Logger log = Logger.getLogger(UserController.class);// 日志文件
	
	/**
	 * 获取用户分页列表
	 * @param pageReq
	 * @param user
	 * @param response
	 * @return
	 * @throws Exception
	 */
	@Function(func="sysmgr.user.query",describe="查询")
	@RequestMapping("/list")
	@RequiresPermissions("sysmgr.user.query")
    public @ResponseBody PageResp<UserBean> pageList(PageReq pageReq, UserQo userQo, HttpServletResponse response) throws Exception {
        PageResp<UserBean> page= userService.findPaging(pageReq);
        return page;
    }
	
	/**
	 * 查询用户详细信息
	 * @param id
	 * @return
	 */
	@RequiresPermissions("sysmgr.user.query")
	@RequestMapping(value="/find",method=RequestMethod.POST)
	public @ResponseBody UserBean find(Integer id){
		UserBean entity=userService.findDetail(id);
		return entity;
	}

	/**
	 * 新增
	 * @param entity
	 * @return
	 */
	@Function(func="sysmgr.user.save",describe="新增")
	@RequiresPermissions("sysmgr.user.save")
	@RequestMapping(value="/save",method=RequestMethod.POST)
	public @ResponseBody OperationResult<?> save(UserBean entity){
		OperationResult<?> result=userService.save(entity);
		return result;
	}
	
	/**
	 * 修改
	 * @param entity
	 * @return
	 */
	@Function(func="sysmgr.user.update",describe="修改")
	@RequiresPermissions("sysmgr.user.update")
	@RequestMapping(value="/update",method=RequestMethod.POST)
	public @ResponseBody OperationResult<?> update(UserBean entity){
		OperationResult<?> result=userService.update(entity);
		return result;
	}
	
	/**
	 * 删除用户
	 * @param id
	 * @return
	 */
	@Function(func="sysmgr.user.delete",describe="删除")
	@RequiresPermissions("sysmgr.user.delete")
	@RequestMapping(value="/delete",method=RequestMethod.POST)
	public @ResponseBody OperationResult<?> delete(Integer id){
		OperationResult<?> result;
		if(id!=null){
			UserBean entity=new UserBean(id);
			result=userService.delete(entity);
		}else{
			result= new OperationResult<Integer>(OperationResultType.QueryNull,0);
		}
		return result;
	}
}

注:@Function注解声明我们Action是什么权限。而在这里我们对每一个Action上又都增加了一个注解@RequiresPermissions,来表示访问这个Action需要类似sysmgr.user.delete这样的权限。当权限不足时会丢出已给异常,被我们的全局异常处理来捕获到。

后台的Action访问权限我们都加上了,再来看看我们前端视图该如何控制。

Shiro标签库

还是以User为例:

<shiro:hasPermission name="sysmgr.user.save">
	<a class="easyui-linkbutton" iconCls="c-icon fa fa-plus" plain="true" onclick="userUtils.openWin()">新增</a>
</shiro:hasPermission>

注:我们仅在需要控制权限的地方增加Shiro标签,即可限制访问。关于Shiro标签库,可以看参考资料中的文章。

好了,其实到这里我们已经基本上实现了Shiro的集成,并学会了怎么使用Shiro进行认证授权。下面来看看我们最后的成果:
admin账号登录
sunnj账号登录

参考资料

  1. SSM(三)Shiro使用详解
  2. Spring文档学习–异常处理(Handling exceptions)以Shiro为例
  3. 第九章 JSP标签——《跟我学Shiro》

发表评论

电子邮件地址不会被公开。 必填项已用*标注