本文将讲述在Spring+SpringMvc项目中使用Shiro来保障系统的安全。关于shiro理论性的东西本文不深入讲解,重点在于在Spring项目中配置和使用shiro.关于理论性的东西,可以看看 张开涛的《跟我学Shiro》。话不多说,先来看看如何配置Shiro。
1 在web.xml中添加一个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>
2 在springMvc.xml (SpringWeb上下文配置文件)中配置 支持Shiro对Controller的方法级AOP安全控制
<!-- 支持Shiro对Controller的方法级AOP安全控制 begin (即支持shiro的注解对权限和角色的控制) -->
<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!-- 当用户访问没有权限信息的服务时跳转到相应的页面 -->
<prop key="org.apache.shiro.authz.UnauthorizedException">error/403</prop>
<!-- 当用户访问因为安全问题抛出异常时跳转到相应的页面 -->
<prop key="java.lang.Throwable">error/500</prop>
</props>
</property>
</bean>
<!-- 支持Shiro对Controller的方法级AOP安全控制 end -->
3 新建一个Spring上下文支持Shiro的配置文件 spring-context-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- shiro的安全数据源 用于用户登录认证 以及授权查询 -->
<bean id="systemAuthorizingRealm" class="com.swx.cn.rbac.realm.SystemAuthorizingRealm">
<property name="service" ref="realmService" />
</bean>
<!-- 为shiro的安全数据源 用于用户登录认证 以及授权查询 通常是从数据库获取数据 -->
<bean id="realmService" class="com.swx.cn.login.service.impl.RealmService" />
<!-- Shiro安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 确认用户访问authc拦截器拦截的路径时用户已经登录,否则跳转到登录页面即loginUrl -->
<property name="loginUrl" value="../../login.jsp" />
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions" />
</property>
</bean>
<!-- Shiro权限过滤器的定义 定义了 拦截的路径及其对应的 拦截器 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/service/login/loginIn = anon <!-- 登录路径不需要登录认证拦截 -->
/service/admin/** = roles[admin] <!-- admin模块必须要有admin角色才能访问 -->
/service/** = authc <!-- 所有service服务都需要登录认证拦截 -->
</value>
</constructor-arg>
</bean>
<!-- 添加 Shiro Spring AOP 权限注解的支持 开始 -->
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="systemAuthorizingRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="cacheManager" />
</bean>
<!-- 缓存管理器 使用 Ehcache 实现 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-local.xml" />
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000" />
<property name="deleteInvalidSessions" value="true" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
</bean>
<!-- 会话 DAO -->
<bean id="sessionDAO"
class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<property name="activeSessionsCacheName" value="activeSessionCache" />
<property name="sessionIdGenerator" ref="sessionIdGenerator" />
</bean>
<!-- 会话 ID 生成器 -->
<bean id="sessionIdGenerator"
class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<!-- 会话 Cookie 模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="180000" />
</bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="defaultCache">
<diskStore path="../temp/jeesite/ehcache" />
<!-- 默认缓存配置. 自动失效:最后一次访问时间间隔300秒失效,若没有访问过自创建时间600秒失效。-->
<defaultCache maxEntriesLocalHeap="1000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
overflowToDisk="true" statistics="true"/>
<!-- 系统缓存 -->
<cache name="sysCache" maxEntriesLocalHeap="1000" eternal="true" overflowToDisk="true" statistics="true"/>
<!-- 用户缓存 -->
<cache name="userCache" maxEntriesLocalHeap="1000" eternal="true" overflowToDisk="true" statistics="true"/>
<!-- 集团缓存 -->
<cache name="corpCache" maxEntriesLocalHeap="1000" eternal="true" overflowToDisk="true" statistics="true"/>
<!-- 内容管理模块缓存 -->
<cache name="cmsCache" maxEntriesLocalHeap="1000" eternal="true" overflowToDisk="true" statistics="true"/>
<!-- 工作流模块缓存 -->
<cache name="actCache" maxEntriesLocalHeap="100" eternal="true" overflowToDisk="true" statistics="true"/>
<!-- 简单页面缓存 -->
<cache name="pageCachingFilter" maxEntriesLocalHeap="1000" eternal="false" timeToIdleSeconds="120"
timeToLiveSeconds="120" overflowToDisk="true" memoryStoreEvictionPolicy="LFU" statistics="true"/>
<!-- 系统活动会话缓存 -->
<cache name="activeSessionsCache" maxEntriesLocalHeap="10000" eternal="true" overflowToDisk="true"
diskPersistent="true" diskExpiryThreadIntervalSeconds="600" statistics="true"/>
</ehcache>
5 到此为止,shiro的环境配置我们已经完成了。下面我们在项目中实现shiro
6 由于在4中需要注册一个systemAuthorizingRealm bean .所以创建一个systemAuthorizingRealm类
/**
* <h3> Shiro 从此类 获取安全数据(如用户、角色、权限)</h3>
* 创建日期: 2017年10月24日
* @author 赵松强
*/
public class SystemAuthorizingRealm extends AuthorizingRealm{
@Autowired
private IRealmService service;
/** 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取当前已登录的用户
Principal principal = (Principal) getAvailablePrincipal(principals);
IUser user = service.getUserByToken(principal.getUserName(),principal.getOfficeId());
if(user == null){
return null;
}else{
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//用户权限信息
// 添加用户权限
info.addStringPermission("user");
List<IMenu> list = service.getMenuList(user);
for (IMenu menu : list){
if (!"".equals(menu.getPermission())){
// 添加基于Permission的权限信息
for (String permission : menu.getPermission().split(",")){
info.addStringPermission(permission);
}
}
}
List<IRole> rolesList = service.getRoleList(user);
// 添加用户角色信息
for (IRole role : rolesList){
info.addRole(role.getRoleEnName());
}
return info;
}
}
/** 用户信息认证回调函数, 登录时调用用于验证用户信息
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
//将默认登录口令转换为带officeid的口令
UsernamePasswordToken token =(UsernamePasswordToken) authcToken;
//根据用户名和officeid获取用户信息
IUser user = service.getUserByToken(token.getUsername(),token.getOfficeid());
if(user == null){
return null;
}
if(user.loginEnAble()){//用户可以登录
//返回的信息中包括了一个Principal,项目中可以通过SecurityUtils.getSubject().getPrincipal()获取该信息
return new SimpleAuthenticationInfo(new Principal(user),
user.getPassword(), getName());
}else{//如果用户已经被禁止登录
throw new AuthenticationException("msg:该帐号被禁止登录.");
}
}
/**
* 用户信息,可以通过 SecurityUtils.getSubject().getPrincipal()获取该信息
*/
public static class Principal implements Serializable {
private String number;//用户编码(工号、学号等)
private String userName;//用户名
private String officeId;//所属组织机构id
private String office;//所属组织机构
public Principal(IUser user){
this.number = user.getNumber();
this.userName = user.getUserName();
this.officeId = user.getOfficeId();
this.office = user.getOffice();
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getOfficeId() {
return officeId;
}
public void setOfficeId(String officeId) {
this.officeId = officeId;
}
public String getOffice() {
return office;
}
public void setOffice(String office) {
this.office = office;
}
}
}
7 在6中引用了一个UsernamePasswordToken登陆扣了类,这个类主要是重写shiro的UsernamePasswordToken增加officeid,用于标识用户所属组织机构id。
/**
* <h3>重写shiro的UsernamePasswordToken增加officeid,用于标识用户所属组织机构id </h3>
* 创建日期: 2017年10月26日
* @author 赵松强
*/
public class UsernamePasswordToken extends org.apache.shiro.authc.UsernamePasswordToken {
private static final long serialVersionUID = 1L;
public UsernamePasswordToken(String officeId,String username,String password){
super.setUsername(username);
super.setPassword(password.toCharArray());
this.officeid = officeId;
}
/**
* 用户所属组织结构的id
*/
private String officeid;
public String getOfficeid() {
return officeid;
}
public void setOfficeid(String officeid) {
this.officeid = officeid;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
}
8 目前为止,我们已经为shiro提供了数据源,我们可以真正使用它了 这里我新建了一个Shiro工具类,简单写了两个使用的方法
public class ShiroUtils {
/**
* 用户登录时调用登录方法
* @param officeId 组织机构id
* @param username 用户名
* @param password 密码
* @return 登录信息
* {@link com.swx.cn.rbac.bean.LoginInfo}
*/
public static LoginInfo login(String officeId,String username,String password){
UsernamePasswordToken token = new UsernamePasswordToken(officeId,username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return new LoginInfo("登录成功",true);
} catch (AuthenticationException e) { //登录失败
String className = e.getClass().getName(), message = "";
if (IncorrectCredentialsException.class.getName().equals(className)
|| UnknownAccountException.class.getName().equals(className)){//用户名或密码错误导致的异常
message = "用户名或密码错误, 请重试.";
}else if (e.getMessage() != null && e.getMessage().contains("msg:")){//非用户名或密码错误导致的异常
message = e.getMessage().replace("msg:", "");
}else{//其他异常
message = "系统出现点问题,请稍后再试!";
e.printStackTrace(); // 输出到控制台
}
return new LoginInfo(message,false);
}
}
/**判断用户是否拥有指定的角色标识
* @param roleName 角色标识
* @return
*/
public static boolean hasRole(String roleName){
Subject subject = SecurityUtils.getSubject();
return subject.hasRole(roleName);
}
/**
* 获取Shiro的session (Shiro自己实现的session机制,注意不是HttpSession)
* @return Shiro session
*/
public static Session getSession(){
Subject subject = SecurityUtils.getSubject();
return subject.getSession(true);
}
}
其实这个类的主要功能都是来自于 SecurityUtils.getSubject() 这个方法返回的 Subject对象。我们可以看看org.apache.shiro.subject.Subject 接口为我们提供了那些使用的方法
public interface Subject {
Object getPrincipal();
PrincipalCollection getPrincipals();
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
boolean hasRole(String roleIdentifier);
boolean[] hasRoles(List<String> roleIdentifiers);
boolean hasAllRoles(Collection<String> roleIdentifiers);
void checkRole(String roleIdentifier) throws AuthorizationException;
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(String... roleIdentifiers) throws AuthorizationException;
void login(AuthenticationToken token) throws AuthenticationException;
boolean isAuthenticated();
boolean isRemembered();
Session getSession();
Session getSession(boolean create);
void logout();
<V> V execute(Callable<V> callable) throws ExecutionException;
void execute(Runnable runnable);
<V> Callable<V> associateWith(Callable<V> callable);
Runnable associateWith(Runnable runnable);
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
boolean isRunAs();
PrincipalCollection getPreviousPrincipals();
PrincipalCollection releaseRunAs();
}
使用上面这个接口提供的功能我们基本上能满足所有我们需要shiro 的功能。
9 完成工具类后我们可以在登录方法中调用工具类中的登录方法了
@Controller
@RequestMapping("/login")
public class LoginController {
@RequestMapping("loginIn")
public String login(HttpServletRequest request, HttpServletResponse response, Model model){
LoginInfo flag = ShiroUtils.login("370000", "郑进", "11111111");
return "login";
}
}
LoginInfo类里面只是简单封装了下登录的结果
public class LoginInfo {
/**
* 登录信息 主要是承载失败信息
*/
private String message;
/**
* 登录结果 true:登录成功 false:登录失败
*/
private boolean result;
//省略setter getter
}
Spring+SpringMVC环境下使用Shiro已经搭建好了,在系统中可以在控制器的方法上使用
@RequiresPermissions("user:view:123") //标识用户必须要有 user:view:123 才能访问此方法
@RequiresRoles("ceshi") //标识用户必须要有 ceshi 角色才能访问此方法
当然也可以通过 Subject接口提供的方法在任何地方进行 权限验证