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