shiro与spring web项目整合
shiro与springweb项目整合在“基于url拦截实现的工程”基础上整合,基于url拦截实现的工程的技术架构是springmvc+mybatis,整合注意两点:
1、shiro与spring整合
2、加入shiro对web应用的支持
1.0 加入shiro的jar包
1.1 web.xml添加shiro Filter
<!-- shiro过虑器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
1.2 创建applicationContext-shiro.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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- Shiro 的Web过滤器 -->
<bean id="shiroFilter"class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager"ref="securityManager" />
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl"value="/login.action" />
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
<property name="filterChainDefinitions">
<value>
<!-- 退出拦截,请求logout.action执行退出操作 -->
/logout.action = logout
<!-- 无权访问页面 -->
/refuse.jsp = anon
<!-- roles[XX]表示有XX角色才可访问 -->
/item/list.action = roles[item],authc
/js/** anon
/images/** anon
/styles/** anon
/validatecode.jsp anon
/item/* authc
<!-- user表示身份认证通过或通过记住我认证通过的可以访问 -->
/** = authc
</value>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm"ref="userRealm" />
</bean>
<!-- 自定义 realm -->
<bean id="userRealm"class="cn.itcast.ssm.realm.CustomRealm1">
</bean>
</beans>
securityManager:这个属性是必须的。
loginUrl:没有登录认证的用户请求将跳转到此地址进行认证,不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
unauthorizedUrl:没有权限默认跳转的页面。
1.2 自定义Realm
public class CustomRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "customRealm";
}
// 支持什么类型的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException{
// 从token中获取用户身份信息
String username = (String) token.getPrincipal();
// 拿username从数据库中查询
// ....
// 如果查询不到则返回null
if (!username.equals("zhang")) {// 这里模拟查询不到
return null;
}
// 获取从数据库查询出来的用户密码
String password = "123";// 这里使用静态数据模拟。。
// 根据用户id从数据库取出菜单
//...先用静态数据
List<SysPermission> menus = newArrayList<SysPermission>();;
SysPermission sysPermission_1 = new SysPermission();
sysPermission_1.setName("商品管理");
sysPermission_1.setUrl("/item/queryItem.action");
SysPermission sysPermission_2 = new SysPermission();
sysPermission_2.setName("用户管理");
sysPermission_2.setUrl("/user/query.action");
menus.add(sysPermission_1);
menus.add(sysPermission_2);
// 构建用户身份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(username);
activeUser.setUsername(username);
activeUser.setUsercode(username);
activeUser.setMenus(menus);
// 返回认证信息由父类AuthenticatingRealm进行认证
SimpleAuthenticationInfo simpleAuthenticationInfo = newSimpleAuthenticationInfo(
activeUser, password, getName());
return simpleAuthenticationInfo;
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// 获取身份信息
ActiveUser activeUser = (ActiveUser)principals.getPrimaryPrincipal();
//用户id
String userid = activeUser.getUserid();
// 根据用户id从数据库中查询权限数据
// ....这里使用静态数据模拟
List<String> permissions = newArrayList<String>();
permissions.add("item:query");
permissions.add("item:update");
// 将权限信息封闭为AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = newSimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
}
1.3 登陆controller代码
// 用户登陆提交
@RequestMapping("/login")
public Stringloginsubmit(Model model, HttpServletRequest request)
throws Exception {
// shiro在认证过程中出现错误后将异常类路径通过request返回
String exceptionClassName = (String) request
.getAttribute("shiroLoginFailure");
if(exceptionClassName!=null){
if (UnknownAccountException.class.getName().equals(exceptionClassName)){
throw new CustomException("账号不存在");
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
throw new CustomException("用户名/密码错误");
} else if("randomCodeError".equals(exceptionClassName)){
throw new CustomException("验证码错误");
} else{
throw new Exception();//最终在异常处理器生成未知错误
}
}
return "login";
}
1.4 跳转首页的controller 代码
由于session由shiro管理,需要修改首页的controller方法,将session中的数据通过model传到页面。
//系统首页 @RequestMapping("/first") public String first(Model model)throws Exception{ //主体 Subject subject = SecurityUtils.getSubject(); //身份 ActiveUser activeUser = (ActiveUser) subject.getPrincipal(); model.addAttribute("activeUser", activeUser); return "/first"; }
1.5 退出
<!-- 退出拦截,请求logout.action执行退出操作 -->
/logout.action = logout
1.6 无权限refuse.jsp
当用户无操作权限,shiro将跳转到refuse.jsp页面。
1.7 shiro过虑器总结
过滤器简称 | 对应的java类 |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数
roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户,身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查
注:
anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
2.0 认证
添加凭证匹配器实现md5加密校验。修改applicationContext-shiro.xml:
<!-- 凭证匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5" /> <property name="hashIterations" value="1" /> </bean> <!-- 自定义 realm --> <bean id="userRealm"class="cn.itcast.ssm.realm.CustomRealm1"> <property name="credentialsMatcher"ref="credentialsMatcher" /> </bean>
2.1 修改Realm 认证方法
修改realm代码从数据库中查询用户身份信息,将sysService注入realm。
public class CustomRealm1 extends AuthorizingRealm { @Autowired private SysService sysService; @Override public String getName() { return "customRealm"; } // 支持什么类型的token @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException{ // 从token中获取用户身份 String usercode = (String) token.getPrincipal(); SysUser sysUser = null; try { sysUser = sysService.findSysuserByUsercode(usercode); } catch (Exception e) { // TODO Auto-generatedcatch block e.printStackTrace(); } // 如果账号不存在 if (sysUser == null) { return null; } // 根据用户id取出菜单 List<SysPermission> menus = null; try { menus = sysService.findMenuList(sysUser.getId()); } catch (Exception e) { // TODO Auto-generatedcatch block e.printStackTrace(); } // 用户密码 String password = sysUser.getPassword(); //盐 String salt = sysUser.getSalt(); // 构建用户身体份信息 ActiveUser activeUser = new ActiveUser(); activeUser.setUserid(sysUser.getId()); activeUser.setUsername(sysUser.getUsername()); activeUser.setUsercode(sysUser.getUsercode()); activeUser.setMenus(menus); SimpleAuthenticationInfo simpleAuthenticationInfo = newSimpleAuthenticationInfo( activeUser, password, ByteSource.Util.bytes(salt),getName()); return simpleAuthenticationInfo; } ..... }
3.0 授权
修改realm代码从数据库中查询权限信息,将sysService注入realm
public class CustomRealm1 extends AuthorizingRealm { @Autowired private SysService sysService; @Override public String getName() { return "customRealm"; } // 支持什么类型的token @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //身份信息 ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal(); //用户id String userid = activeUser.getUserid(); //获取用户权限 List<SysPermission> permissions = null; try { permissions = sysService.findSysPermissionList(userid); } catch (Exception e) { // TODO Auto-generatedcatch block e.printStackTrace(); } //构建shiro授权信息 SimpleAuthorizationInfo simpleAuthorizationInfo = newSimpleAuthorizationInfo(); for(SysPermission sysPermission:permissions){ simpleAuthorizationInfo.addStringPermission(sysPermission.getPercode()); } return simpleAuthorizationInfo; } }
3.1 对controller开启AOP
在springmvc.xml中配置shiro注解支持,可在controller方法中使用shiro注解配置权限:
<!-- 开启aop,对类代理 --> <aop:config proxy-target-class="true"></aop:config> <!-- 开启shiro注解支持 --> <bean class=" org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager"ref="securityManager" /> </bean>
3.2 权限注解控制
商品查询controller方法添加权限(item:query):
// 查询商品列表
@RequestMapping("/queryItem")
@RequiresPermissions("item:query")
public ModelAndView queryItem()throws Exception {
上边代码@RequiresPermissions("item:query")表示必须拥有“item:query”权限方可执行。
3.3 标签的介绍
Jsp页面添加: <%@tagliburi="http://shiro.apache.org/tags" prefix="shiro"%> 标签名称 标签条件(均是显示标签内容) <shiro:authenticated> 登录之后 <shiro:notAuthenticated> 不在登录状态时 <shiro:guest> 用户在没有RememberMe时 <shiro:user> 用户在RememberMe时 <shiro:hasAnyRoles name="abc,123" > 在有abc或者123角色时 <shiro:hasRole name="abc"> 拥有角色abc <shiro:lacksRole name="abc"> 没有角色abc <shiro:hasPermission name="abc"> 拥有权限资源abc <shiro:lacksPermission name="abc"> 没有abc权限资源 <shiro:principal> 显示用户身份名称 <shiro:principal property="username"/> 显示用户身份中的属性值
3.4 jsp添加权限标签
<shiro:hasPermission name="item:update"> xxxx </shiro:hasPermission>
4.0 shiro的缓存
shiro每次授权都会通过realm获取权限信息,为了提高访问速度需要添加缓存,第一次从realm中读取权限数据,之后不再读取,这里Shiro和Ehcache整合。
4.1 加入Ehcache 的jar包
4.2 配置cacheManager
<!-- 安全管理器 --> <bean id="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm"ref="userRealm" /> <property name="cacheManager"ref="cacheManager"/> </bean> <!-- 缓存管理器 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile"value="classpath:shiro-ehcache.xml"/> </bean>
4.3 配置shiro-ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--diskStore:缓存数据持久化的目录地址 -->
<diskStore path="F:\develop\ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
4.4 清除缓存
当用户权限修改后,用户再次登陆shiro会自动调用realm从数据库获取权限数据,如果在修改权限后想立即清除缓存则可以调用realm的clearCache方法清除缓存。
realm中定义clearCached方法:
//清除缓存 public void clearCached(){ PrincipalCollectionprincipals = SecurityUtils.getSubject().getPrincipals(); super.clearCache(principals); }在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,调用clearCached方法
5.0 session管理
在applicationContext-shiro.xml中配置sessionManager:
<!-- 安全管理器 --> <bean id="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm"ref="userRealm" /> <property name="sessionManager"ref="sessionManager" /> </bean> <!-- 会话管理器 --> <bean id="sessionManager"class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- session的失效时长,单位毫秒 --> <property name="globalSessionTimeout" value="600000"/> <!-- 删除失效的session --> <property name="deleteInvalidSessions" value="true"/> </bean>
6.0 验证码
6.1 自定义 FormAuthenticationFilter
需要在验证账号和名称之前校验验证码。
public class MyFormAuthenticationFilter extendsFormAuthenticationFilter {
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { // 校验验证码 // 从session获取正确的验证码 HttpSession session =((HttpServletRequest)request).getSession(); //页面输入的验证码 String randomcode = request.getParameter("randomcode"); //从session中取出验证码 String validateCode = (String)session.getAttribute("validateCode"); if (randomcode!=null && validateCode!=null) { if(!randomcode.equals(validateCode)) { // randomCodeError表示验证码错误 request.setAttribute("shiroLoginFailure", "randomCodeError"); //拒绝访问,不再校验账号和密码 return true; } } return super.onAccessDenied(request, response, mappedValue); } }
6.2 FormAuthenticationFilter配置
修改applicationContext-shiro.xml中对FormAuthenticationFilter的配置
在shiroFilter中添加filters:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="filters">
<map>
<!--FormAuthenticationFilter是基于表单认证的过虑器 -->
<entry key="authc"value-ref="formAuthenticationFilter" />
</map>
</property>
.....
.....
6.3 formAuthenticationFilter 定义
<!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 -->
<bean id="formAuthenticationFilter"
class="org.apache.shiro.web.filter.authc.MyFormAuthenticationFilter">
<!-- 表单中账号的input名称 -->
<property name="usernameParam"value="username" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam"value="password" />
</bean>
6.4 登录页面添加验证码
<TR> <TD>验证码:</TD> <TD><input id="randomcode"name="randomcode" size="8" /> <img id="randomcode_img" src="${baseurl}validatecode.jsp" alt="" width="56" height="20"align='absMiddle' /> <a href=javascript:randomcode_refresh()>刷新</a></TD> </TR>
6.5 配置validatecode.jsp匿名访问
修改applicationContext-shiro.xml:
7.0 记住我
7.1 用户身份实现java.io.Serializable接口
7.2 配置rememberMeManager
<!-- 安全管理器 -->
<bean id="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm"ref="userRealm" />
<property name="sessionManager"ref="sessionManager" />
<property name="cacheManager"ref="cacheManager"/>
<!-- 记住我 -->
<property name="rememberMeManager"ref="rememberMeManager"/>
</bean>
<!--rememberMeManager管理器 -->
<bean id="rememberMeManager"class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie"ref="rememberMeCookie" />
</bean>
<!-- 记住我cookie -->
<bean id="rememberMeCookie"class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<!-- 记住我cookie生效时间30天 -->
<property name="maxAge"value="2592000" />
</bean>
7.3 FormAuthenticationFilter配置
<bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.MyFormAuthenticationFilter"> <!-- 表单中账号的input名称 --> <property name="usernameParam"value="usercode" /> <!-- 表单中密码的input名称 --> <property name="passwordParam"value="password" /> <property name="rememberMeParam"value="rememberMe"/> </bean>
7.4 登录页面添加记住我按钮
在login.jsp中添加“记住我”checkbox。
<TR> <TD></TD> <TD> <input type="checkbox"name="rememberMe" />自动登陆 </TD> </TR>提醒:账号zhangsan 密码:111111