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

时间:2023-01-23 21:55:41

最近在写一个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依赖包:

<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
1.2、在web.xml添加shiro过滤器,这个过滤器的位置貌似没有限制:

<!-- 这里的 filter-name 必须在 applicationContext.xml中配置一个 bean id='shiroFilter' -->
<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>
顺带在web.xml最后配置下404界面

<!-- 出错页面定义 -->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/pages/sys/error_500.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/pages/sys/error_500.jsp</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/pages/sys/error_404.jsp</location>
</error-page>
<error-page>
<error-code>406</error-code>
<location>/pages/sys/error_404.jsp</location>
</error-page>
1.3、在applicationContext.xml中添加shiro配置:

<!--shiro配置-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/home"/>
<property name="unauthorizedUrl" value="/403"/>
<property name="filters">
<map>
<entry key="logout" value-ref="logoutFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/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
</value>
</property>
</bean>
<!-- 退出登录过滤器 -->
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!--重定向-->
<property name="redirectUrl" value="/login"/>
</bean>
<!--配置权限核心管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!--默认缓存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
说明:
<property name="loginUrl" value="/login"/> --如果你没有登录,访问有登录验证的请求,都会跳转到/login页面,/login需要在controller中写好如:
@RequestMapping(value = "login",method = RequestMethod.GET)
public String login(){
return "sys/login";
}
还有:
<property name="successUrl" value="/home"/> --登录成功跳转的页面<property name="unauthorizedUrl" value="/403"/> --没有权限跳转的页面
登录失败的跳转页面在后面讲.
这里设置logoutFilter(注销登录过滤器),该Filter中做了重定向到"/login",不做重定向的话页面会直接跳到"/",读者注意,这个也是很多文章都没有讲到的地方
realm配置:是shiro的核心,包括登录验证和权限验证,这两个核心一定要有概念
<property name="realm" ref="myRealm"/>
这里要编码自己的realm:它是继承AuthorizingRealm对象,代码如下(类名的首字母变小写后必须与 ref="myRealm" 一致,Spring基本功):
/** * Created by QuSongTao@低调火药 on 20160412. */@Servicepublic 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冲突错误,注意不要加错了)
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/><!-- 启用SHIRO注解 --><bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">    <property name="proxyTargetClass" value="true" /></bean><bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">    <property name="securityManager" ref="securityManager"/></bean>
2、权限控制:
2.1、登录权限验证(这个看似简单,其实"超级难"的,不少文章都介绍得很简单,还有就是不少文章会对登录的login请求单独写个controller来接收,然后做登录验证,这是错误的写法),来吧,先写一个登录的jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %><!DOCTYPE html><% String path = request.getContextPath();  String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";%><html><head>  <title></title>  <base href="<%=basePath%>"/>  <link rel="stylesheet" type="text/css" href="resources/style/subPage.css"/></head><body><div style="...">  <div style="...">    <form id="loginForm" action="login" method="post">      <div class="login_header">智慧人生</div>      <div style="...">        <div style="...">          <div style="...">            &nbsp;&nbsp;:          </div>          <!-- 特别注意 name="username" 这个必须写死,不能做更改--> <input type="text" name="username" style="..."/>          <div style="...">            &nbsp;&nbsp;:          </div>          <!-- 特别注意 name="password" 这个必须写死,不能做更改--> <input type="password" name="password" style="..."/>        </div>      </div>      <div style="...">        <input type="submit" style="..." value="&nbsp;&nbsp;" />      </div>      <div style="...">        ${message} </div>    </form>  </div></div></body></html>
这里本人不建议直接把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中配置的
<property 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<MenuBean> 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", "欢迎:<span style='color:red;'><shiro:principal type='com.*.logistics.struct.domain.StbUsers' property='name' /></span>" , false, "about.gif");} //注意用单引号

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