
我们做项目一般很难避开权限及验证的问题,这就需要一个完整的权限框架来支持项目。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);
会进入到我们AuthRealm
的doGetAuthenticationInfo()
方法来认证。其他的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进行认证授权。下面来看看我们最后的成果: