SSM整合安全框架Shiro案例
By: Date: 2017年5月6日 Categories: 程序 标签:,

我们做项目一般很难避开权限及验证的问题,这就需要一个完整的权限框架来支持项目。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》

发表回复

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