最近在写一个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依赖包:
1.2、在web.xml添加shiro过滤器,这个过滤器的位置貌似没有限制: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}
顺带在web.xml最后配置下404界面shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true shiroFilter /*
1.3、在applicationContext.xml中添加shiro配置: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
说明: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 2016年04月12日. */ @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="...">
这里本人不建议直接把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直接通过 SecurityUtils. getSubject ().getPrincipal() ;获取user信息,这个user在哪里被赋值呢,其实在 MyRealm.java 的 doGetAuthenticationInfo方法中已经被赋值进去了.红色部分,getMenuAll(){ StbUsers user = (StbUsers) SecurityUtils.getSubject().getPrincipal(); return menuService.getMenuList(user.getId()); }
/** * 登录认证 * @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");} //注意用单引号
欢迎:<shiro:principal type="com.*.logistics.struct.domain.StbUsers" property="name" />
/** * 权限认证 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //todo--本文只做了登录验证,权限验证没有做 return null; }做配置和编码,并在方法级上加入shiro注解即可,棘突信息请参考其它资料,,至于那个BUG,知道的大神请留言