Spring MVC 集成 Apache Shiro权限控制-测试可行

最近在写一个webapp,基础的功能写完了,最后差一个权限的控制,在网上也找了不少关于权限控制的文章(说实话,不怎么好,有些地方没有写清楚,让读者一头雾水),对于J2EE项目,总的来说有2个目前比较流行的权限控制框架,一个是Spring Security,另外一个就是Apache Shiro,关于两者的优劣,网友们都做了大量的比对,从轻量级易上手的角度,我们选择Apache Shiro,废话少说,首先项目要解决的大问题是防止无权访问、非法访问、强制流氓访问等一系列我们不情愿的访问。

本项目IDE是intellij IDEA 2016 ,本文不再讲解spring mvc的配置,这需要读者另外完成

1、工作准备:

1.1、配置shiro maven依赖包

1.2、在web.xml添加shiro过滤器

1.3、在applicationContext.xml中添加shiro配置

1.4、在mvc-dispacher-servlet.xml(有的叫:spring-mvc.xml)添加shiro配置


2、权限控制:

2.1、登录权限验证

2.2、登录成功跳转

2.3、登录失败跳转

2.4、注销登录跳转

2.5、session超时设置

2.6、Controller中session信息获取

2.7、shiro标签在JSP页面获取登录session信息


3、其它事项

3.1、本文只做了登录验证功能的说明,权限验证只做了简单描述,详细信息读者可参考其他资料

3.2、不少网友提出的变态BUG,本文没有解决,BUG为:第一次打开网站时输入错误的不存在地址,比如我的正确登录地址为:http://192.168.1.100:8080/sys/login,用户输入http://192.168.1.100:8080/sys/login123,这时候shiro会拦截该地址进行登录认证,认证失败跳转到登录界面,即:http://192.168.1.100:8080/sys/login,BUG来了,用户在登录界面输入正确的用户名密码登录系统时,会跳到404页面(即第一次登录的请求地址:http://192.168.1.100:8080/sys/login123),大神们有谁能解决这个BUG,请留言.


1.1 配置shiro maven依赖包:



    org.apache.shiro
    shiro-core
    ${shiro.version}


    org.apache.shiro
    shiro-spring
    ${shiro.version}


    org.apache.shiro
    shiro-web
    ${shiro.version}


    org.apache.shiro
    shiro-ehcache
    ${shiro.version}
1.2、在web.xml添加shiro过滤器,这个过滤器的位置貌似没有限制:



    shiroFilter
    org.springframework.web.filter.DelegatingFilterProxy
    
        targetFilterLifecycle
        true
    



    shiroFilter
    /*
顺带在web.xml最后配置下404界面



    java.lang.Throwable
    /pages/sys/error_500.jsp


    500
    /pages/sys/error_500.jsp


    404
    /pages/sys/error_404.jsp


    406
    /pages/sys/error_404.jsp
1.3、在applicationContext.xml中添加shiro配置:


id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    name="securityManager" ref="securityManager"/>
    name="loginUrl" value="/login"/>
    name="successUrl" value="/home"/>
    name="unauthorizedUrl" value="/403"/>
    name="filters">
        
            key="logout" value-ref="logoutFilter"/>
        
    
    name="filterChainDefinitions">
        
            /resources/** = anon   # 静态资源随便访问
            /login = authc         # 登录需要登录验证
            /logout = logout       # logout交给logoutFilter处理然后重定向

            /welcome = anon        # 欢饮页面随便访问
            / = anon               # 根随便访问
            /** = authc            # 其它的所有请求都需要进行登录验证
            # some example chain definitions:
            # /admin/** = authc, roles[admin]
            # /docs/** = authc, perms[document:read]
            # more URL-to-FilterChain definitions here
        
    


id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
    
    name="redirectUrl" value="/login"/>


id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    name="realm" ref="myRealm"/>
    name="cacheManager" ref="cacheManager"/>


id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
说明:
name="loginUrl" value="/login"/> --如果你没有登录,访问有登录验证的请求,都会跳转到/login页面,/login需要在controller中写好如:
 
  
@RequestMapping(value = "login",method = RequestMethod.GET)
public String login(){
    return "sys/login";
}
还有:
 
  
name="successUrl" value="/home"/> --登录成功跳转的页面
name="unauthorizedUrl" value="/403"/> --没有权限跳转的页面
登录失败的跳转页面在后面讲.
这里设置logoutFilter(注销登录过滤器),该Filter中做了重定向到"/login",不做重定向的话页面会直接跳到"/",读者注意,这个也是很多文章都没有讲到的地方
realm配置:是shiro的核心,包括登录验证和权限验证,这两个核心一定要有概念
 
  
name="realm" ref="myRealm"/>
这里要编码自己的realm:它是继承AuthorizingRealm对象,代码如下(类名的首字母变小写后必须与 ref="myRealm" 一致,Spring基本功):
 
  
/**
 * Created by QuSongTao@低调火药 on 20160412.
 */
@Service
public class MyRealm extends AuthorizingRealm {
    private static Logger LOG = LoggerFactory.getLogger(MyRealm.class);
    @Autowired
    private UsersService usersService;
    /**
     * 权限认证
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //todo--本文只做了登录验证,权限验证没有做
        return null;
    }

    /**
     * 登录认证
     * @param at
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken at) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) at;
        // 通过表单接收的用户名
        String username = token.getUsername();
        if (username != null && !"".equals(username)) {
            StbUsers user = usersService.findUser(username).get(0);
            //用户存在性验证
            if (user != null) {
//                return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
              //密码正确性验证
                return new SimpleAuthenticationInfo(user, user.getPassword(), user.getName());
            }
        }
        return null;
    }
}
重写两个方法,一个权限验证,一个登录验证
 
  
1.4、在mvc-dispacher-servlet.xml(有的叫:spring-mvc.xml)添加shiro配置:(有的文章说的很模糊,不少的人把这个放在applicationContext.xml中,那样会报shiro注解与Spring冲突错误,注意不要加错了)
 
  
id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
    name="proxyTargetClass" value="true" />

class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    name="securityManager" ref="securityManager"/>
2、权限控制:
2.1、登录权限验证(这个看似简单,其实"超级难"的,不少文章都介绍得很简单,还有就是不少文章会对登录的login请求单独写个controller来接收,然后做登录验证,这是错误的写法),来吧,先写一个登录的jsp
 
  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
html>
<%
  String path = request.getContextPath();
  String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";

%>


  
  href="<%=basePath%>"/>
  rel="stylesheet" type="text/css" href="resources/style/subPage.css"/>



style="...">
style="...">
id="loginForm" action="login" method="post">
class="login_header">智慧人生
style="...">
style="...">
style="...">   :
type="text" name="username" style="..."/>
style="...">   :
type="password" name="password" style="..."/>
style="..."> type="submit" style="..." value="  " />
style="..."> ${message}
 
  
这里本人不建议直接把css样式写在html中,虽然与本文内容无关
特别注意:username和password的name属性定义,为什么?为什么?为什么?重要的事说三遍,首先看shiro的登录机制和源码,用户发起login请求首先被shiroFilter拦截到,之后交给MyRealm.java 的 doGetAuthenticationInfo(AuthenticationToken at)方法来处理请求的表单,之前请求表单的信息又是通过FormAuthenticationFilter装配一遍,看FormAuthenticationFilter源码:
package org.apache.shiro.web.filter.authc;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FormAuthenticationFilter extends AuthenticatingFilter {
    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";
    public static final String DEFAULT_USERNAME_PARAM = "username";
    public static final String DEFAULT_PASSWORD_PARAM = "password";
    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";
    private static final Logger log = LoggerFactory.getLogger(FormAuthenticationFilter.class);
    private String usernameParam = "username";
    private String passwordParam = "password";
    private String rememberMeParam = "rememberMe";
    private String failureKeyAttribute = "shiroLoginFailure";
    ..........
看到没有,直接将login请求中的username和password参数的值装进来,这是之所以在登录jsp页面中为何要写死name=" username "和 name=" password "的原因,之后 MyRealm.java 的 doGetAuthenticationInfo方法 开始处理登录验证,看方法中注释.
2.2、登录成功跳转
此时登录请求还没有进到controller里面,关键的东西来了,也就是shiro的巧妙之处,MyRealm.java 的 doGetAuthenticationInfo方法处理登录成功后,直接跳转到shiroFIlter中配置的
name="successUrl" value="/home"/>
/home页面,(很多网友无法跳转到配置的登录成功页面大都是因为jsp页面中form表单的name属性问题,很多文章都没有说这个事),
2.3、登录失败跳转
如果登录验证失败,注意,注意注意,doGetAuthenticationInfo方法处理登录失败后,这时候会把请求交给controller,因此我们只需要定义一个登录验证失败后的跳转页面即可,controller代码如下:
 
  
@RequestMapping(value = "login",method = RequestMethod.POST)
public String loginFail(ModelMap model){
    model.addAttribute("message", "用户不存在或密码错误!");
    return "sys/login";
}
登录验证失败后直接返回到login页面并告诉用户用户密码错误!,注意这个controller只有shiro验证失败后才到这里,否则永远不会到这里,很多兄弟像下面这样写(极力不推荐):
 
  
@RequestMapping(value = "login",method = RequestMethod.POST)
public String login(HttpServletRequest request,ModelMap model){
    String usernmae = request.getParameter("username");
    String password = request.getParameter("password");
    //user servcie...
    //开始数据库验证用户名密码
    if(notFoundUser){
        model.addAttribute("message", "用户不存!");
        return "sys/login";
    }
    if(passwordError){
        model.addAttribute("message", "密码不一致!");
        return "sys/login";
    }
    //最后交给shiro
    SecurityUtils.getSubject().login(new UsernamePasswordToken(usernmae,password));
    model.addAttribute("message", "登录成功!");
    return "sys/homepage";
}

这样子写就可能导致登录成功后跳转到"/"页面去.
2.4、注销登录跳转
这个只需在filter中配置logout的重定向即可,否则会跳转到"/",很多文章中也没有提到
2.5、session超时设置
见过配置在xml中的,个人感觉那样有点麻烦,直接通过代码实现,本文登录成功后跳到/home页面,controller中定义session超时时长,简单粗暴.
@RequestMapping(value = "home",method = RequestMethod.GET)
public String  toHomePage(HttpServletRequest request,ModelMap model){
    //登录后设置session会话超时时间为24小时
    SecurityUtils.getSubject().getSession().setTimeout(24*3600*1000);
    return "sys/homepage";
}
2.6、Controller中session信息获取
大体上写下session的获取,有这样的场景,用户登录后,session保存了用户的信息,在一些业务controller中要获取用户的信息用:SecurityUtils,代码如下:
比如有个获取顶级菜单的请求:
public class StbUsers {
    private int id;
    private String loginId;
    private String name;
    private String disabled;
    private String mobile;
    private String email;
    private String des;
    private String password;
    private Integer createBy;
    private Timestamp createDate;
    private Integer lastModifiedBy;
    private Timestamp lastModifiedDate;
    private String authType;

    geter... seter...    

/**
 * 获取顶级菜单列表请求
 * @return
 */
@RequestMapping(value = "menu/list", method = RequestMethod.GET)
public List getMenuAll(){
    StbUsers user = (StbUsers) SecurityUtils.getSubject().getPrincipal();
    return menuService.getMenuList(user.getId());
}
直接通过 SecurityUtils. getSubject ().getPrincipal() ;获取user信息,这个user在哪里被赋值呢,其实在 MyRealm.java 的 doGetAuthenticationInfo方法中已经被赋值进去了.红色部分,
/**
     * 登录认证
     * @param at
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken at) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) at;
        // 通过表单接收的用户名
        String username = token.getUsername();
        if (username != null && !"".equals(username)) {
            StbUsers user = usersService.findUser(username).get(0);
            //用户存在性验证
            if (user != null) {
//                return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
                //密码正确性验证
                return new SimpleAuthenticationInfo(user, user.getPassword(), user.getName());
            }
        }
        return null;
    }

那么在任何controller或者Service里面都可以获取登录用户的信息.
2.7、shiro标签在JSP页面获取登录session信息
首先在jsp页面中引入shiro标签,放在页面最上面
 
  
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags"%>
使用的时候,获取登录用户信息代码,这个可以嵌入在js里,也可以正常的写在html里
js中的样子:
 
  
function attachMenu() {
    if (myMenu != null) return;
    myMenu = myLayout.attachMenu({
        icons_path: "resources/dhtmlx/common/18/"
    });
    myMenu.addNewSibling(null, "id01", "欢迎:<shiro:principal type='com.*.logistics.struct.domain.StbUsers' property='name' />" , false, "about.gif");} //注意用单引号

html中的样子

    欢迎:<shiro:principal type="com.*.logistics.struct.domain.StbUsers" property="name" />
效果图
3 权限的验证和shiro注解的使用在
/**
 * 权限认证
 * @param principalCollection
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //todo--本文只做了登录验证,权限验证没有做
    return null;
}
做配置和编码,并在方法级上加入shiro注解即可,棘突信息请参考其它资料,,至于那个BUG,知道的大神请留言
本文关键的代码已放到csdn下载资源中"Apache shiro权限控制基础配置代码.rar"
 
  

你可能感兴趣的:(架构设计)