1.整合SSM并且实现用户登陆和菜单权限
具体整合参考https://www.jianshu.com/p/d0ffe2505216
2.将shiro整合到ssm中
a)添加shiro的相关jar包
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
b)在web.xml中添加shiro配置
<!-- 配置shiro的过滤器,通过代理配置,对象由spring容器来创建,但是交由servlet容器来管理-->
<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>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- shiro 过滤器 end -->
c)springmvc.xml
<!--设置开启shiro的注解,例如@RequiresRoles("admin")-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--配置异常处理,处理shiro的权限和认证异常跳转-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!--授权异常-->
<prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
<!--认证异常-->
<prop key="org.apache.shiro.authz.UnauthenticatedException">/toLogin</prop>
</props>
</property>
</bean>
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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--注入自定义的Realm-->
<bean id="customRealm" class="com.lyh.realm.customRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!--配置凭证匹配器,加凭证匹配器后要对实体类序列化-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="2"/>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm"/>
</bean>
<!-- 配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. -->
<bean id="lifecycleBeanPostProcessor"
class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!--配置ShiroFilter-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--登入页面,当访问需要认证的资源时,如果没有认证将自动跳转到该url,不配置默认到根目录下的login.jsp-->
<property name="loginUrl" value="/"/>
<!--登入成功页面,配置认证成功后跳转的url,通常不设置,因为如果不设置认证成功后跳转上一个URL-->
<!-- <property name="successUrl" value="/index.jsp"/>-->
<!-- 配置用户没有访问权限时跳转的页面-->
<property name="unauthorizedUrl" value="/refuse"/>
<!--URL的拦截,配置shiro的过滤器链-->
<property name="filterChainDefinitions" >
<value>
/=anon
/toLogin=anon
/logout = logout <!--logout默认跳转到根目录下,可以重新指定-->
/refuse=anon
/**=authc
</value>
</property>
</bean>
<!--重新定义logout过滤器让其使用自己指定的跳转url,id不能改变只能是logout-->
<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/refuse"/>
</bean>
</beans>
Shiro中默认的过滤器
d)写好自定义realm
import com.lyh.domain.User;
import com.lyh.mapper.UserMapper;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
public class customRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Override
public String getName() {
return "customRealm";
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = principalCollection.getPrimaryPrincipal().toString();
User userByName = userMapper.getUserByName(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(userByName.getRoles());
info.addStringPermission("user:add");
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
User userByName = userMapper.getUserByName(username);
ByteSource pwd_salt = ByteSource.Util.bytes(userByName.getPassword_salt());
return new SimpleAuthenticationInfo(username,userByName.getPassword(),pwd_salt,getName());
}
}
controller代码:
import com.lyh.mapper.UserMapper;
import com.lyh.domain.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpSession;
@Controller
public class UserController {
@Autowired
UserMapper userMapper;
@RequestMapping("/")
public ModelAndView hostPage(){
ModelAndView modelAndView = new ModelAndView("login");
return modelAndView;
}
@RequestMapping("/getUser/{id}")
@RequiresRoles("admin")
public ModelAndView getUser(@PathVariable Integer id){
ModelAndView modelAndView = new ModelAndView("index");
User userById = userMapper.getUserById(id);
modelAndView.addObject("user",userById);
return modelAndView;
}
@RequestMapping("/toLogin")
public ModelAndView login(String userName, String passwd, HttpSession session) {
ModelAndView modelAndView = new ModelAndView();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, passwd);
try {
subject.login(token);
} catch (Exception e) {
modelAndView.addObject("msg", "用户名或密码错误!");
modelAndView.setViewName("login");
return modelAndView;
}
User userByName = userMapper.getUserByName(userName);
session.setAttribute("login_user",userByName);
modelAndView.setViewName("index");
return modelAndView;
}
@RequestMapping("/refuse")
public ModelAndView refuse() {
ModelAndView modelAndView = new ModelAndView("refuse");
return modelAndView;
}
}
注解名称 | 解释 |
---|---|
@RequiresAuthentication | 表示当前Subject已经通过login身份验证;即Subject.isAuthenticated() == true;否则就拦截 |
@RequiresUser | 表示当前Subject已经通过login身份验证或通过记住我登录;否则就拦截 |
@RequiresGuest | 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份 |
@RequiresRoles(value ={"admin", "user"}, logical=Logical.AND) | 表示当前Subject需要同时(由Logical.AND体现)拥有admin和user角色;否则拦截 |
@RequiresPermissions(vale={"user:a","user:b"}, logical=Logical.OR) | 表示当前Subject需要拥有user:a或者(由Logical.OR体现)user:b角色;否则拦截 |
JSP页面授权
Shiro提供了JSTL标签用于在JSP/GSP页面进行权限控制;首先需要导入标签库:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
标签名称 | 作用 |
---|---|
<shiro:guest> | 用户没有身份验证时显示相应的信息,即游客访问信息 |
<shiro:user> | 用户已经身份验证、记住我登录后显示相应的信息,未登录用户将会拦截 |
<shiro:authenticated> | 用户已经身份验证通过,即Subject.isAuthenticated() == true;未登录或记住我登录的都会拦截 |
<shiro:notAuthenticated> | 用户已经身份验证通过,但是Subject.isAuthenticated() == false,即可能是通过记住我登录的 |
<shiro:principal> | 显示用户身份信息,默认调用Subject.getPrincipal()获取用户登录信息 |
<shiro:hasRole> | 如:<shiro:hasRole name="admin">,如果当前Subject有admin角色就显示数据,类似于@RequiresRoles()注解;否则就拦截 |
<shiro:hasAnyRole> | 如:<shiro:hasAnyRole name="admin,user">,如果当前Subject有admin或user角色就显示数据,类似于@RequireRoles(Logical=Logical.OR)注解;否则将就拦截 |
<shiro:lackRole> | 如果当前Subject没有角色就显示数据 |
<shiro:hasPermission> | 如:<shiro:hasPermission name="user:create">,如果当前Subject有user:create权限,就显示数据;否则就拦截 |
<shiro:lacksPermission> | 如:<shiro:lacksPermission name="user:create">,如果当前Subject没有user:create权限,就显示数据;否则拦截 |
3.缓存
每次权限检查都会到数据库中获取权限,这样效率很低,可以通过设置缓存来解决该问题。shiro可以和ehcache或者redis集成。在这里使用ehcache来缓存数据。
a)导入ehcache相关jar包。shiro默认集成了一个ehcache配置文件,也可以自己添加一个配置文件
pom文件:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
b)Shiro也集成了缓存机制,例如Shiro提供了CachingRealm,提供了一些基础的缓存实现。首先我们要开启Shiro的缓存管理,在XML中进行如下配置:
<!--配置ehcache,指定ehcache配置文件路径,不指定就用默认的配置-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache/ehcache.xml"/>
</bean>
按照路径配置ehcache文件ehcache/ehcache.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache >
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
</ehcache>
设置SecurityManager的cacheManager:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm"/>
<!--把上面注入好的cacheManager设置进securityManager-->
<property name="cacheManager" ref="cacheManager"/>
</bean>
在自定义的Realm实现中配置缓存的实现(也可以不配置已经默认开启缓存):
<!--注入自定义的Realm-->
<bean id="customRealm" class="com.lyh.realm.customRealm">
<!-- 使用credentialsMatcher实现密码验证服务 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<!-- 是否启用缓存 -->
<property name="cachingEnabled" value="true"/>
<!-- 是否启用身份验证缓存 -->
<property name="authenticationCachingEnabled" value="true"/>
<!-- 缓存AuthenticationInfo信息的缓存名称 -->
<property name="authenticationCacheName" value="authenticationCache"/>
<!-- 是否启用授权缓存,缓存AuthorizationInfo信息 -->
<property name="authorizationCachingEnabled" value="true"/>
<!-- 缓存AuthorizationInfo信息的缓存名称 -->
<property name="authorizationCacheName" value="authorizationCache"/>
</bean>
c)如果在运行过程中,主体的权限发生变化,那么应该从spring容器中调用realm中的清理缓存方法。
//清理缓存
public void clearCache() {
Subject subject = SecurityUtils.getSubject();
PrincipalCollection principals = subject.getPrincipals();
super.clearCache(principals);
}
处理修改权限的业务代码后调用上述方法
userMapper.addPermission(permission_username);
customRealm.clearCache();
4.会话管理
<!--配置会话管理器sessionManager-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 设置全局会话过期时间:默认30分钟 -->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 删除无效session -->
<property name="deleteInvalidSessions" value="true"/>
<!-- 是否启用sessionIdCookie,默认是启用的 -->
<property name="sessionIdCookieEnabled" value="true"/>
<!-- 会话Cookie -->
<property name="sessionIdCookie" ref="sessionIdCookie"/>
</bean>
<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid"/>
<!-- 如果设置为true,则客户端不会暴露给服务端脚本代码,有助于减少某些类型的跨站脚本攻击 -->
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/><!-- maxAge=-1表示浏览器关闭时失效此Cookie -->
</bean>
还要将sessionManager
注入到SecurityManager
中:
<!--配置securityManager,设置自定义realm,缓存,会话-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm"/>
<!--把上面注入好的cacheManager设置进securityManager-->
<property name="cacheManager" ref="cacheManager"/>
<!-- 注入sessionManager -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
5.实现Remember功能
在Shiro会话管理时我们就讲到会话的功能,例如:Shiro实现了RememberMe记住我的功能,当用户在登录页面中勾选了记住我,再浏览器关闭后再次访问系统发现是可以直接登录的;但是如果没有实现这一功能,Shiro默认设置浏览器关闭后立即清除缓存,那么再次打开浏览器要重新进行登录。
拓展
RememberMe和使用Subject.login(token)登录是有所不同的,RememberMe是使用缓存Cookie的技术实现的登录,在前面讲到的一些权限注解中就说到了两者的区别。
RememberMe的配置实现
在配置文件中写入:
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="2592000"/><!-- 30天 -->
</bean>
<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- cipherKey是加密rememberMe Cookie的密匙,默认AES算法 -->
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>
在SecurityManager
中设置rememberMeManager
:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
在登录表单中添加一个checkbox:
<input type="checkbox" name="remember">请记住我
如果用户勾选了这个复选框,点击登录按钮提交后台的参数中会多一个remember参数,且值是on(如果用户没有勾选,提交表单中就不存在这个参数);所以我们修改Controller的登录方法:
@RequestMapping("doLogin")
public ModelAndView login(String username, String pwd, String remember, HttpSession session){
ModelAndView modelAndView = new ModelAndView();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, pwd);
if (remember != null){
if (remember.equals("on")) {
//说明选择了记住我
token.setRememberMe(true);
} else {
token.setRememberMe(false);
}
}else{
token.setRememberMe(false);
}
try {
subject.login(token);
} catch (Exception e) {
modelAndView.addObject("msg", "用户名或密码错误!");
modelAndView.setViewName("login");
return modelAndView;
}
User userByName = userMapper.selectByUsername(username);
session.setAttribute("login_user",userByName);
modelAndView.setViewName("user");
return modelAndView;
}
把filterChainDefinitions中的拦截器从authc改成user拦截器区别见上图
<!--URL的拦截,配置shiro的过滤器链-->
<property name="filterChainDefinitions" >
<value>
/goLogin=anon
/doLogin=anon
/logout = logout <!--logout默认跳转到根目录下,可以重新指定-->
/refuse=anon
/**=user
</value>
</property>
注意:
走shiro的记住我方式登录后发现session数据丢失,解决办法我们自己写一个拦截器,验证当通过记住我方式登录后,把session加进去
实现HandlerInterceptor中的preHandle()方法
截取登录时的subject,判断是否是通过rememberMe登录,将principal手动添加到session中
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lyh.domain.User;
import com.lyh.mapper.UserMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 拦截rememberMe的请求,添加user到session中
* @author echo
*
*/
public class RememberMeInterceptor implements HandlerInterceptor {
@Autowired
UserMapper userMapper;
public RememberMeInterceptor() {
// TODO Auto-generated constructor stub
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("测试方法进入preHandle");
// 获取session中的subject
Subject subject = SecurityUtils.getSubject();
// 判断是不是通过记住我登录
if( !subject.isAuthenticated() && subject.isRemembered()) {
Session session = subject.getSession();
User user= (User)subject.getPrincipal();
User userByName = userMapper.getUserByName(user.getUsername());
session.setAttribute("login_user",userByName);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
}
在springmvc.xml文件配置拦截器:
<mvc:interceptors>
<!--如果配置了多个拦截器,则按顺序执行 -->
<mvc:interceptor>
<!-- /**表示所有url包括子url路径 -->
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/goLogin"/>
<mvc:exclude-mapping path="/toLogin"/>
<mvc:exclude-mapping path="/logout"/>
<bean class="com.lyh.utils.RememberMeInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
注意jsp标签和拦截器<shiro:user>与<shiro:authenticated>和user与authc的区别不要用错。