apache shiro 是功能强大而且容易集成的开源权限框架,它可以完成认证、受权、加密、会话管理等功能。认证和受权为权限控制的核心,简单来讲,“认证”就是证实“你是谁?” Web 应用程序通常作法是经过表单提交的用户名及密码达到认证目的。“受权”便是"你能作什么?",不少系统经过资源表的形式来完成用户能作什么。关于 shiro 的一系列特征及优势,不少文章已有列举,这里再也不逐一赘述,本文首先会简单的讲述 shiro 和spring该如何集成,重点介绍 shiro 的几个实用功能和一些 shiro 的扩展知识。html
因为 spring 在 java web 应用里普遍使用,在项目中使用 spring 给项目开发带来的好处有不少,spring 框架自己就是一个很是灵活的东西,而 shrio 的设计模式,让 shiro 集成 spring 并不是难事。java
首先在web.xml里,经过 spring 的 org.springframework.web.filter.DelegatingFilterProxy 定义一个 filter ,让全部可访问的请求经过一个主要的 shiro 过滤器。该过滤器自己是极为强大的,容许临时的自定义过滤器链基于任何 URL 路径表达式执行。git
web.xml:github
<!-- shiro security filter --> <filter> <filter-name>shiroSecurityFilter</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>shiroSecurityFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping>
接下来在你的 applicationContext.xml 文件中定义 web 支持的 SecurityManager 和刚刚在 web.xml 定义的 shiroSecurityFilter 便可完成 shiro 和 spring 的集成。web
applicationContext.xml:正则表达式
<!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- 默认的链接拦截配置 --> <property name="filterChainDefinitions"> <value> /login = authc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <...> </bean>
提示: org.apache.shiro.spring.web.ShiroFilterFactoryBean 的 id 名称必须和 web.xml 的 filter-name 一致spring
ShiroFilterFactoryBean 相似预初始化shiro的一个 java bean,主要做用是配置一些东西让 shiro 知道要作些什么动做,好比,经过以上的配置,要访问http://localhost:port/porject/index,必须当前用户拥有 permission 为 security:index 才能访问,不然将会跳转到指定的loginUrl。当登陆时使用 FormAuthenticationFilter 来作登陆等等。sql
ShiroFilterFactoryBean 的 filterChainDefinitions 是对系统要拦截的连接url作配置,好比,我系统中有一条连接为 http://localhost:prot/project/add ,须要当前用户存在角色为admin或者拥有 permission 为 system:add 的才能访问该连接。须要配置以下:数据库
/add = role[admin], perms[security:index]
提示:Shiro支持了权限(permissions)概念。权限是功能的原始表述,如:开门、建立一个博文、删除jsmith用户等。经过让权限反映应用的原始功能,在改变应用功能时,你只须要改变权限检查。进而,你能够在运行时按需将权限分配给角色或用户。express
若是不配置任何东西在里面的话,shiro会起不到安全框架的做用。但若是将整个系统的全部连接配置到 filterChainDefinitions 里面会有不少,这样做的作法会不靠谱。因此,应该经过动态的、可配置的形式来作 filterChainDefinitions,该功能会在动态filterChainDefinitions里说明如何经过数据库来建立动态的filterChainDefinitions。
在独立应用程序和 web 应用程序中,你可能想为安全检查使用 shiro 的注释(例如,@RequiresRoles,@RequiresPermissions 等等)。这须要 shiro 的 spring AOP 集成来扫描合适的注解类以及执行必要的安全逻辑。如下是如何使用这些注解的。只需添加这两个 bean:
<aop:aspectj-autoproxy proxy-target-class="true" /> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
对于这两个bean,在base-framework里添加在applicationContext-mvc.xml中,这样作是为了启用 shiro 注解时仅在 spring mvc 的 controller 层就能够了,没必要在 service 和 dao 中也使用该注解。
到这里,shiro 和 spring 集成的关键点只有这么点东西。最重要的接口在 securityManager 中。securityManager 管理了认证、受权,session 等 web 安全的重要类,首先来完成认证、受权方面的功能。
在 shiro 里,认证,主要是知道“你是谁”,受权,是给于你权限去“作什么”。因此,在完成认证和受权以前,咱们要构造最经典的权限3张表去完成这些事,但在这里画表要图片,总体感也很丑。因此,以 hibernate 实体的方式去说明表的结构:
首先,经典3张表须要用户、组、资源这3个实体,而3个实体的关系为多对多关系:
/** * 用户实体 * @author maurice * */ @Entity @Table(name="TB_USER") public class User extends IdEntity{ private static final long serialVersionUID = 1L; //登陆名称 private String username; //登陆密码 private String password; //用户所在的组 @ManyToMany(fetch=FetchType.LAZY) @JoinTable( name = "TB_GROUP_USER", joinColumns = { @JoinColumn(name = "FK_USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "FK_GROUP_ID") } ) private List<Group> groupsList = new ArrayList<Group>(); //----------------getting/setting----------------// }
** * 组实体 * * @author maurice * */ @Entity @Table(name="TB_GROUP") public class Group extends IdEntity{ private static final long serialVersionUID = 1L; //名称 private String name; //用户成员 @ManyToMany(fetch=FetchType.LAZY) @JoinTable( name = "TB_GROUP_USER", joinColumns = { @JoinColumn(name = "FK_GROUP_ID") }, inverseJoinColumns = { @JoinColumn(name = "FK_USER_ID") } ) private List<User> membersList = new ArrayList<User>(); //拥有访问的资源 @ManyToMany(fetch=FetchType.LAZY) @JoinTable( name = "TB_GROUP_RESOURCE", joinColumns = { @JoinColumn(name = "FK_GROUP_ID") }, inverseJoinColumns = { @JoinColumn(name = "FK_RESOURCE_ID") } ) private List<Resource> resourcesList = new ArrayList<Resource>(); //shiro role 字符串 private String role; //shiro role连定义的值 private String value; //----------------getting/setting----------------// }
** * 资源实体 * * @author maurice * */ @Entity @Table(name="TB_RESOURCE") public class Resource extends IdEntity{ private static final long serialVersionUID = 1L; //名称 private String name; //action url private String value; //资源所对应的组集合 @ManyToMany(fetch=FetchType.LAZY) @JoinTable( name = "TB_GROUP_RESOURCE", joinColumns = { @JoinColumn(name = "FK_RESOURCE_ID") }, inverseJoinColumns = { @JoinColumn(name = "FK_GROUP_ID") } ) private List<Group> groupsList = new ArrayList<Group>(); //shiro permission 字符串 private String permission; //----------------getting/setting----------------// }
经过以上3个 hibernate 实体,构建出了如下表结构,有了这些表,作认证和受权是很是简单的一件事:
表名 | 表说明 |
---|---|
TB_USER | 用户表 |
TB_GROUP | 组表 |
TB_RESOURCE | 资源表 |
TB_GROUP_USER | 用户与组的多对多中间表 |
TB_GROUP_RESOURCE | 组与资源的多对多中间表 |
初始化数据假设是这样:
TB_USER | ||
id | username | password |
17909124407b8d7901407be4996c0001 | admin | admin |
TB_GROUP | |||
id | name | role | value |
17909124407b8d7901407be4996c0002 | 超级管理员 |
TB_RESOURCE | |||
id | name | permission | value |
17909124407b8d7901407be4996c0003 | 添加用户 | perms[user:add] | /user/add/** |
TB_GROUP_USER | |
FK_USER_ID | FK_GROUP_ID |
17909124407b8d7901407be4996c0001 | 17909124407b8d7901407be4996c0002 |
TB_GROUP_RESOURCE | |
FK_GROUP_ID | FK_RESOURCE_ID |
17909124407b8d7901407be4996c0002 | 17909124407b8d7901407be4996c0003 |
首先要认识的第一个对象是securityManager所管理的 org.apache.shiro.realm.Realm 接口,realm 担当 shiro 和你的应用程序的安全数据之间的“桥梁”或“链接器”。但它实际上与安全相关的数据(如用来执行认证及受权)的用户账户交互时,shiro 从一个或多个为应用程序配置的 realm 中寻找许多这样的东西。
在这个意义上说,realm 本质上是一个特定安全的 dao:它封装了数据源的链接详细信息,使 shiro 所需的相关数据可用。当配置 shiro 时,你必须指定至少一个 realm 用来进行身份验证和受权。securityManager 可能配置多个 realms,但至少必须有一个。
shiro 提供了当即可用的 realms 来链接一些安全数据源(即目录),如LDAP、关系数据库(JDBC)、文本配置源等。若是默认地 realm 不符合你的需求,你能够插入你本身的 realm 实现来表明自定义的数据源。
在base-framework里就使用了本身的realm来完成受权和认证工做,realm接口有不少实现类,包括缓存、JdbcRealm、JndiLdapRealm,而JdbcRealm、JndiLdapRealm都是继承AuthorizingRealm类,AuthorizingRealm类有两个抽象方法:
/** * * 访问连接时的受权方法 * */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals); /** * 用户认证方法 * */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
doGetAuthenticationInfo方法的做用是在用户进行登陆时经过该方法去作认证工做(你是谁),doGetAuthenticationInfo 方法里的 AuthenticationToken 参数是一个认证令牌,装载着表单提交过来的数据,因为 shiro 的认证 filter 默认为 FormAuthenticationFilter,经过 filter 建立的令牌为 UsernamePasswordToken类,该类里面包含了表单提交上来的username、password、remeberme等信息。
doGetAuthorizationInfo方法的做用是在用户认证完成后(登陆完成后),对要访问的连接作受权工做。好比刚刚在上面配置的 spring xml 文件里有那么一句话:
<!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <...> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接 --> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- 默认的链接拦截配置 --> <property name="filterChainDefinitions"> <value> ... /index = perms[security:index] </value> </property> </bean>
当用户登陆成功后会跳转到 successUrl 这个连接,即:http://localhost:port/index。那么这个index又要当前用户存在permission 为 security:index 才能进入,因此,当登陆完成跳转 successUrl 时,会进入到 doGetAuthorizationInfo 方法里进行一次受权,让 shiro 了解该连接在当前认证的用户里是否能够访问,若是能够访问,那就执行接入到index,不然就会跳转到unauthorizedUrl。
了解以上状况,首先咱们建立UserDao和ResourceDao类来作数据访问工做:
@Repository public class UserDao extends BasicHibernateDao<User, String> { /**经过登陆账号获取用户实体**/ public User getUserByUsername(String username) { return findUniqueByProperty("username", username); } }
@Repository public class ResourceDao extends BasicHibernateDao<Resource, String> { /**经过用户id获取用户全部的资源集合**/ public List<Resource> getUserResource(String id) { String h = "select rl from User u left join u.groupsList gl left join gl.resourcesList rl where u.id=?1"; return distinct(h, id); } }
而后在建立一个类,名叫JdbcAuthenticationRealm,并继承AuthorizingRealm这个抽象类,实现它的抽象方法:
public class JdbcAuthenticationRealm extends AuthorizingRealm{ @Autowired private UserDao userDao; @Autowired private ResourceDao resourceDao; /** * 用户登陆的身份验证方法 * */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; String username = usernamePasswordToken.getUsername(); if (username == null) { throw new AccountException("用户名不能为空"); } //经过登陆账号获取用户实体 User user = userDao.getUserByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } return new SimpleAuthenticationInfo(user,user.getPassword(),getName()); } /** * * 当用户进行访问连接时的受权方法 * */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { if (principals == null) { throw new AuthorizationException("Principal对象不能为空"); } User user = principals.oneByType(User.class); List<Resource> resource = resourceDao.getUserResource(user.getId()); //获取用户相应的permission List<String> permissions = CollectionUtils.extractToList(resource, "permission",true); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); addPermissions(info, permissions); return info; } /** * 经过资源集合,将集合中的permission字段内容解析后添加到SimpleAuthorizationInfo受权信息中 * * @param info SimpleAuthorizationInfo * @param authorizationInfo 资源集合 */ private void addPermissions(SimpleAuthorizationInfo info,List<Resource> authorizationInfo) { //解析当前用户资源中的permissions List<String> temp = CollectionUtils.extractToList(authorizationInfo, "permission", true); List<String> permissions = getValue(temp,"perms\\[(.*?)\\]"); //添加默认的permissions到permissions if (CollectionUtils.isNotEmpty(defaultPermission)) { CollectionUtils.addAll(permissions, defaultPermission.iterator()); } //将当前用户拥有的permissions设置到SimpleAuthorizationInfo中 info.addStringPermissions(permissions); } /** * 经过正则表达式获取字符串集合的值 * * @param obj 字符串集合 * @param regex 表达式 * * @return List */ private List<String> getValue(List<String> obj,String regex){ List<String> result = new ArrayList<String>(); if (CollectionUtils.isEmpty(obj)) { return result; } Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(StringUtils.join(obj, ",")); while(matcher.find()){ result.add(matcher.group(1)); } return result; } }
完成后修改applicationContext.xml文件:
<!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接 --> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] /changePassword = perms[security:change-password] </value> </property> </bean>
以上代码首先从doGetAuthenticationInfo读起,首先。假设咱们有一个表单
<form action="${base}/login" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="checkbox" name="remeberMe" /> <input type="submit" value="提交"/> </form>
提示: input标签的全部name属性不必定要写死 username,password,remeberMe。能够在 FormAuthenticationFilter 修改
当点击提交时,shiro 会拦截此次的表单提交,由于在配置文件里已经说明,/login 由 authc 作处理,就是:
<!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <...> <!-- 默认的链接拦截配置 --> <property name="filterChainDefinitions"> <value> /login = authc ... </value> </property> </bean>
而 authc 就是 shiro 的 FormAuthenticationFilter 。shiro 首先会判断 /login 此次请求是否为post请求,若是是,那么就交给 FormAuthenticationFilter 处理,不然将不作任何处理。
当 FormAuthenticationFilter 接收到要处理时。那么 FormAuthenticationFilter 首先会根据表单提交过来的请求参数建立一个UsernamePasswordToken,而后获取一个 Subject 对象,由Subject去执行登陆。
提示: Subject 实质上是一个当前执行用户的特定的安全“视图”。鉴于“User”一词一般意味着一我的,而一个 Subject 能够是一我的,但它还能够表明第三方服务,daemon account,cron job,或其余相似的任何东西——基本上是当前正与软件进行交互的任何东西。
全部 Subject 实例都被绑定到(且这是必须的)一个 SecurityManager 上。当你与一个 Subject 交互时,那些交互做用转化为与 SecurityManager 交互的特定 subject 的交互做用。
Subject执行登陆时,会将UsernamePasswordToken传入到Subject.login方法中。在通过一些小小的处理过程后(如:是否启用了认证缓存,若是是,获取认证缓存,执行登陆,不在查询数据库),会进入到 doGetAuthenticationInfo方法里,而在doGetAuthenticationInfo方法作的事情就是:
经过用户名获取当前用户
经过当前用户和用户密码建立一个 SimpleAuthenticationInfo 而后去匹配密码是否正确
在SimpleAuthenticationInfo对象里的密码为数据库里面的用户密码,返回SimpleAuthenticationInfo后 shiro 会根据表单提交的密码和 SimpleAuthenticationInfo 的密码去作对比,若是彻底正确,就表示认证成功,当成功后,会重定向到successUrl这个连接。
当重定向到 index 时,会进入到 perms过滤器,就是 shiro 的PermissionsAuthorizationFilter,由于配置文件里已经说明,就是:
<!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <...> <!-- 默认的链接拦截配置 --> <property name="filterChainDefinitions"> <value> ... /index = perms[security:index] </value> </property> </bean>
PermissionsAuthorizationFilter的工做主要是判断当前subject是否有足够的权限去访问index,判断条件有:
判断subject是否已认证,若是没认证,返回登陆页面,就是在配置文件里指定的loginUrl。
若是认证了,判断当前用户是否存未受权,若是没有就去受权,当受权时,就会进入到 doGetAuthorizationInfo 方法
若是已经认证了。就判断是否存在xx连接的permission,若是有,就进入,不然重定向到未受权页面,就是在配置文件里指定的unauthorizedUrl
那么,认证咱们上面已经认证过了。就会进入到第二个判断,第二个判断会跑到了doGetAuthorizationInfo方法,而doGetAuthorizationInfo方法里作了几件事:
获取当前的用户
经过用户id获取用户的资源集合
将资源实体集合里的permission获取出来造成一个List
将用户拥有的permission放入到SimpleAuthorizationInfo对象中
doGetAuthorizationInfo 返回 SimpleAuthorizationInfo 对象的做用是让 shiro 的 AuthorizingRealm 逐个循环里面的 permission 和当前访问连接的 permission 去作匹配,若是匹配到了,就表示当前用户能够访问本次请求的连接,不然就重定向到未受权页面。
实现认证和受权功能继承AuthorizingRealm已经能够达到效果,可是要注意几点就是:
表单提交的action要和filterChainDefinitions的一致。
filterChainDefinitions的“/login = authc”这句话的左值要和loginUrl属性一致。
表单提交必需要post方法。
完成认证和受权后如今的缺陷在于filterChainDefinitions都是要手动去一个个配置,一个系统那么多连接都要写上去很是不靠谱,下面将介绍如何使用资源表动态去构建filterChainDefinitions。
动态 filterChainDefinitions 是为了可以经过数据库的数据,将 filterChainDefinitions 构造出来,而不在是一个个手动的写入到配置文件中,在shiro的 ShiroFilterFactoryBean 启动时,会经过 filterChainDefinitions 的配置信息构形成一个Map,在赋值到filterChainDefinitionMap 中,shiro的源码以下:
/** * A convenience method that sets the {@link #setFilterChainDefinitionMap(java.util.Map) filterChainDefinitionMap} * property by accepting a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs). * Each key/value pair must conform to the format defined by the * {@link FilterChainManager#createChain(String,String)} JavaDoc - each property key is an ant URL * path expression and the value is the comma-delimited chain definition. * * @param definitions a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs) * where each key/value pair represents a single urlPathExpression-commaDelimitedChainDefinition. */ public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); //did they explicitly state a 'urls' section? Not necessary, but just in case: Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { //no urls section. Since this _is_ a urls chain definition property, just assume the //default section contains only the definitions: section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } setFilterChainDefinitionMap(section); }
提示:Ini.Section 该类是一个 Map 子类。
ShiroFilterFactoryBean 也提供了设置 filterChainDefinitionMap 的方法,配置 filterChainDefinitions 和 filterChainDefinitionMap 二者只需一个便可。
在实现动态 filterChainDefinitions 时,须要借助 spring 的 FactoryBean 接口去作这件事。spring 的 FactoryBean 接口是专门暴露bean对象的接口,经过接口的 getObject() 方法获取bean实例,也能够经过 getObjectType() 方法去指定bean的类型,让注解Autowired可以注入或在 spring 上下文中 getBean()方法直接经过class去获取该bean。
那么,继续用上面的经典三张表的资源数据访问去动态构造 filterChainDefinitions。 首先建立一个 ChainDefinitionSectionMetaSource 类并实现 FactoryBean 的方法和在resourceDao中添加一个获取全部资源的方法,以下:
@Repository public class ResourceDao extends BasicHibernateDao<Resource, String> { /**经过用户id获取用户全部的资源集合**/ public List<Resource> getUserResource(String id) { String h = "select rl from User u left join u.groupsList gl left join gl.resourcesList rl where u.id=?1"; return distinct(h, id); } /**获取有时有资源**/ public List<Resource> getAllResource() { return getAll(); } }
/** * 借助spring {@link FactoryBean} 对apache shiro的premission进行动态建立 * * @author maurice * */ public class ChainDefinitionSectionMetaSource implements FactoryBean<Ini.Section>{ @Autowired private ResourceDao resourceDao; //shiro默认的连接定义 private String filterChainDefinitions; /** * 经过filterChainDefinitions对默认的连接过滤定义 * * @param filterChainDefinitions 默认的接过滤定义 */ public void setFilterChainDefinitions(String filterChainDefinitions) { this.filterChainDefinitions = filterChainDefinitions; } @Override public Section getObject() throws BeansException { Ini ini = new Ini(); //加载默认的url ini.load(filterChainDefinitions); Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } //循环数据库资源的url for (Resource resource : resourceDao.getAll()) { if(StringUtils.isNotEmpty(resource.getValue()) && StringUtils.isNotEmpty(resource.getPermission())) { section.put(resource.getValue(), resource.getPermission()); } } return section; } @Override public Class<?> getObjectType() { return Section.class; } @Override public boolean isSingleton() { return true; } }
ChainDefinitionSectionMetaSource 类,重点在 getObject() 中,返回了一个 shiro 的 Ini.Section 首先Ini类加载了filterChainDefinitions的配置信息(因为有些连接不必定要放到数据库里,也能够经过直接写在配置文件中)。经过ini.load(filterChainDefinitions);一话构形成了/login key = authc value等信息。那么shiro就知道了login这个url须要使用authc这个filter去拦截。完成以后,经过resourceDao的getAll()方法将全部数据库的信息再次叠加到Ini.Section中(在tb_resource表中的数据为:/user/add/** = perms[user:add]),造成了最后的配置。
完成该以上工做后,修改 spring 的 applicationContext.xml,当项目启动时,你会发如今容器加载spring内容时,会进入到ChainDefinitionSectionMetaSource,若是使用maven的朋友,进入到shiro的源码放一个断点,你会看到tb_resource表的/user/add/** = perms[user:add]已经构造到了filterChainDefinitionMap里。
applicationContext.xml修改成:
<!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = authc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean>
经过修改和添加以上三个文件,完成了动态 filterChainDefinitions 具体的过程在base-framework的showcase的base-curd项目下有例子,若是看不懂。能够根据例子去理解。
验证码登陆在web开发中最多见,shiro对于验证码登陆的功能没有支持,但shiro的设计模式让开发人员自定义一个小小的验证码登陆不会很难。base-framework的showcase的base-curd项目所扩展的验证码登陆需求是:当用户登陆失败次数达到指标时,才出现验证码。
经过该需求,咱们回到上面提到的 FormAuthenticationFilter,该filter是专门作认证用的filter,因此本人第一时间想到扩展它,若是有更好的实现方式但愿可以分享。
实现验证码登陆,咱们首先建立一个CaptchaAuthenticationFilter类,并继承FormAuthenticationFilter。FormAuthenticationFilter最须要重写的方法有:
/**执行登陆**/ protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception /**当登陆失败时所响应的方法**/ protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response); /**当登陆成功时所响应的方法**/ protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception;
因此CaptchaAuthenticationFilter类应该这样实现:
/** * 验证码登陆认证Filter * * @author maurice * */ @Component public class CaptchaAuthenticationFilter extends FormAuthenticationFilter{ /** * 默认验证码参数名称 */ public static final String DEFAULT_CAPTCHA_PARAM = "captcha"; /** * 默认在session中存储的登陆错误次数的名称 */ private static final String DEFAULT_LOGIN_INCORRECT_NUMBER_KEY_ATTRIBUTE = "incorrectNumber"; //验证码参数名称 private String captchaParam = DEFAULT_CAPTCHA_PARAM; //在session中的存储验证码的key名称 private String sessionCaptchaKeyAttribute = DEFAULT_CAPTCHA_PARAM; //在session中存储的登陆错误次数名称 private String loginIncorrectNumberKeyAttribute = DEFAULT_LOGIN_INCORRECT_NUMBER_KEY_ATTRIBUTE; //容许登陆错误次数,当登陆次数大于该数值时,会在页面中显示验证码 private Integer allowIncorrectNumber = 1; /** * 重写父类方法,在shiro执行登陆时先对比验证码,正确后在登陆,不然直接登陆失败 */ @Override protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception { Session session = SystemVariableUtils.createSessionIfNull(); //获取登陆错误次数 Integer number = (Integer) session.getAttribute(getLoginIncorrectNumberKeyAttribute()); //首次登陆,将该数量记录在session中 if (number == null) { number = new Integer(1); session.setAttribute(getLoginIncorrectNumberKeyAttribute(), number); } //若是登陆次数大于allowIncorrectNumber,须要判断验证码是否一致 if (number > getAllowIncorrectNumber()) { //获取当前验证码 String currentCaptcha = (String) session.getAttribute(getSessionCaptchaKeyAttribute()); //获取用户输入的验证码 String submitCaptcha = getCaptcha(request); //若是验证码不匹配,登陆失败 if (StringUtils.isEmpty(submitCaptcha) || !StringUtils.equals(currentCaptcha,submitCaptcha.toLowerCase())) { return onLoginFailure(this.createToken(request, response), new AccountException("验证码不正确"), request, response); } } return super.executeLogin(request, response); } /** * 重写父类方法,当登陆失败将异常信息设置到request的attribute中 */ @Override protected void setFailureAttribute(ServletRequest request,AuthenticationException ae) { if (ae instanceof IncorrectCredentialsException) { request.setAttribute(getFailureKeyAttribute(), "用户名密码不正确"); } else { request.setAttribute(getFailureKeyAttribute(), ae.getMessage()); } } /** * 重写父类方法,当登陆失败次数大于allowIncorrectNumber(容许登陆错误次数)时,将显示验证码 */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { Session session = SystemVariableUtils.getSession(); Integer number = (Integer) session.getAttribute(getLoginIncorrectNumberKeyAttribute()); session.setAttribute(getLoginIncorrectNumberKeyAttribute(),++number); return super.onLoginFailure(token, e, request, response); } /** * 重写父类方法,当登陆成功后,将allowIncorrectNumber(容许登错误录次)设置为0,重置下一次登陆的状态 */ @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { Session session = SystemVariableUtils.getSession(); session.removeAttribute(getLoginIncorrectNumberKeyAttribute()); session.setAttribute("sv", subject.getPrincipal()); return super.onLoginSuccess(token, subject, request, response); } //---------------------------------getter/setter方法----------------------------------// }
CaptchaAuthenticationFilter类重点的代码在executeLogin方法和onLoginFailure方法中。当执行登陆时,会在session中建立一个"登陆错误次数"属性,当该属性大于指定的值时才去匹配验证码,不然继续调用FormAuthenticationFilter的executeLogin方法执行登陆。
当登陆失败时(onLoginFailure方法)会获取"登陆错误次数",而且加1。直到登陆成功后,将"登陆错误次数"属性从session中移除。
提示setFailureAttribute方法的做用是当出现用户名密码错误时提示中文出去,这样会友好些。
因此,在登陆界面的html中用freemarker的话就这样写:
<form action="${base}/login" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="checkbox" name="remeberMe" /> <!--当登陆错误次数大于1时,出现验证码 --> <#if Session.incorrectNumber?? && Session.incorrectNumber gte 1> <input type="text" name="captcha" id="captcha" > <img id="captchaImg" src="get-captcha" /> </#if> <input type="submit" value="提交"/> </form>
完成后在修改applicationContext.xml便可:
<!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean>
提示: applicationContext.xml修改了ShiroFilterFactoryBean的filters属性,在filters属性里添加了一个自定义的 captchaAuthenticationFilter , 名字叫 captchaAuthc 在 filterChainDefinitions 里将 /login = authc 该为 /login = captchaAuthc。
经过添加CaptchaAuthenticationFilter类和修改applicationContext.xml文件,完成了验证码,具体的过程在base-framework的showcase的base-curd项目下有例子,若是看不懂。能够根据例子去理解。
在1.2 shiro 认证、受权中提到,realm 担当 shiro 和你的应用程序的安全数据之间的“桥梁”或“链接器”。必需要存在一个。当一个应用程序配置了两个或两个以上的 realm 时,ModularRealmAuthenticator 依靠内部的 AuthenticationStrategy 组件来肯定这些认证尝试的成功或失败条件。如:若是只有一个 realm 验证成功,但全部其余的都失败,这被认为是成功仍是失败?又或者必须全部的 realm 验证成功才被认为样子成功?又或者若是一个 realm 验证成功,是否有必要进一步调用其余 realm ? 等等。
AuthenticationStrategy: 是一个无状态的组件,它在身份验证尝试中被询问4 次(这4 次交互所需的任何须要的状态将被做为方法参数):
在任何Realm 被调用以前被询问。
在一个单独的Realm 的getAuthenticationInfo 方法被调用以前当即被询问。
在一个单独的Realm 的getAuthenticationInfo 方法被调用以后当即被询问。
在全部的Realm 被调用后询问。
另外,AuthenticationStrategy 负责从每个成功的 realm 汇总结果并将它们“捆绑”到一个单一的 AuthenticationInfo 再现。这最后汇总的 AuthenticationInfo 实例就是从 Authenticator 实例返回的值以及 shiro 所用来表明 Subject 的最终身份ID 的值(即Principals(身份))。
shiro 有 3 个具体的AuthenticationStrategy 实现:
AuthenticationStrategy 类 | 描述 |
AtLeastOneSuccessfulStrategy | 若是一个(或更多)Realm 验证成功,则总体的尝试被认为是成功的。若是没有一个验证成功,则总体尝试失败。 |
FirstSuccessfulStrategy | 只有第一个成功验证的Realm 返回的信息将被使用。全部进一步的Realm 将被忽略。若是没有一个验证成功,则总体尝试失败。 |
AllSucessfulStrategy | 为了总体的尝试成功,全部配置的Realm 必须验证成功。若是没有一个验证成功,则总体尝试失败。 |
ModularRealmAuthenticator 默认的是AtLeastOneSuccessfulStrategy 实现,由于这是最常所需的方案。若是你不喜欢,你能够配置一个不一样的方案:
<!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="authenticator"> <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <!-- 认证策略使用FirstSuccessfulStrategy策略 --> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy" /> </property> <!-- 多realms配置 --> <property name="realms"> <list> <value> <ref bean="jdbcAuthenticationRealm" /> <ref bean="otherAuthenticationRealm" /> </value> </list> </property> </bean> </property> </bean>
那么如今存在这样的需求,让shiro 的多 realms 就有了发挥余地:
在用户登陆时,先从本系统数据库获取用户信息。若是获取获得用户,就执行进行认证。
若是获取不到用户,去第三方应用接口或者其余数据库获取用户。
若是是从第三方应用接口或者其余数据库获取的用户,将用户插入到本系统的用户表中,并赋给它一些本系统的权限。
但问题是:受权和认证的接口AuthorizingRealm须要实现两个方法,但这个需求提到的是认证而已,受权却要统一进行受权。因此,在多realms都继承AuthorizingRealm会出现不少复制粘贴的受权代码。因此,写一个公用的受权抽象类会比较好些。固然,这个看需求而定。
那么定义 AuthorizationRealm 抽象类让多realms继承它,完成各各realms本身的受权:
/** * apache shiro 的公用受权类 * * @author maurice * */ public abstract class AuthorizationRealm extends AuthorizingRealm{ @Autowired private ResourceDao resourceDao; private List<String> defaultPermission = Lists.newArrayList(); /** * 设置默认permission * * @param defaultPermissionString permission 若是存在多个值,使用逗号","分割 */ public void setDefaultPermissionString(String defaultPermissionString) { String[] perms = StringUtils.split(defaultPermissionString,","); CollectionUtils.addAll(defaultPermission, perms); } /** * 设置默认permission * * @param defaultPermission permission */ public void setDefaultPermission(List<String> defaultPermission) { this.defaultPermission = defaultPermission; } /** * * 当用户进行访问连接时的受权方法 * */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { if (principals == null) { throw new AuthorizationException("Principal对象不能为空"); } User user = (User)principals.fromRealm(getName()).iterator().next(); List<Resource> resource = resourceDao.getUserResource(user.getId()); //获取用户相应的permission List<String> permissions = CollectionUtils.extractToList(resource, "permission",true); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //添加用户拥有的permission addPermissions(info,authorizationInfo); return info; } /** * 经过资源集合,将集合中的permission字段内容解析后添加到SimpleAuthorizationInfo受权信息中 * * @param info SimpleAuthorizationInfo * @param authorizationInfo 资源集合 */ private void addPermissions(SimpleAuthorizationInfo info,List<Resource> authorizationInfo) { //解析当前用户资源中的permissions List<String> temp = CollectionUtils.extractToList(authorizationInfo, "permission", true); List<String> permissions = getValue(temp,"perms\\[(.*?)\\]"); //添加默认的permissions到permissions if (CollectionUtils.isNotEmpty(defaultPermission)) { CollectionUtils.addAll(permissions, defaultPermission.iterator()); } //将当前用户拥有的permissions设置到SimpleAuthorizationInfo中 info.addStringPermissions(permissions); } /** * 经过正则表达式获取字符串集合的值 * * @param obj 字符串集合 * @param regex 表达式 * * @return List */ private List<String> getValue(List<String> obj,String regex){ List<String> result = new ArrayList<String>(); if (CollectionUtils.isEmpty(obj)) { return result; } Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(StringUtils.join(obj, ",")); while(matcher.find()){ result.add(matcher.group(1)); } return result; } }
本系统的认证:
/** * * apache shiro 的身份验证类 * * @author maurice * */ public class JdbcAuthenticationRealm extends AuthorizationRealm{ @Autowired private UserDao userDao; /** * 用户登陆的身份验证方法 * */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; String username = usernamePasswordToken.getUsername(); if (username == null) { throw new AccountException("用户名不能为空"); } //经过登陆账号获取用户实体 User user = userDao.getUserByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } return new SimpleAuthenticationInfo(user,user.getPassword(),getName()); } }
第三方应用和其余数据库的认证:
public class otherAuthenticationRealm extends AuthorizationRealm{ /** * 用户登陆的身份验证方法 * */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户 //插入到本系统 //返回SimpleAuthenticationInfo. } }
完成后在修改applicationContext.xml便可:
<!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 自定义shiro的realm数据库身份验证 --> <bean id="otherAuthenticationRealm" class="domain.OtherAuthenticationRealm" /> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="authenticator"> <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <!-- 多realms配置 --> <property name="realms"> <list> <value> <ref bean="jdbcAuthenticationRealm" /> <ref bean="otherAuthenticationRealm" /> </value> </list> </property> </bean> </property> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean>
在base-framework中没有多realms例子,若是存在什么问题。能够到这里提问。
在许多应用程序中性能是相当重要的。缓存是从第一天开始第一个创建在 shiro 中的一流功能,以确保安全操做保持尽量的快。然而,缓存做为一个概念是 shiro 的基本组成部分,实现一个完整的缓存机制是安全框架核心能力以外的事情。为此,shiro 的缓存支持基本上是一个抽象的(包装)API,它将“坐”在一个基本的缓存机制产品(例如,Ehcache,OSCache,Terracotta,Coherence,GigaSpaces,JBossCache 等)之上。这容许 shiro 终端用户配置他们喜欢的任何缓存机制。
shiro 有三个重要的缓存接口:
CacheManager - 负责全部缓存的主要管理组件,它返回 Cache 实例。
Cache - 维护key/value 对。
CacheManagerAware - 经过想要接收和使用 CacheManager 实例的组件来实现。
CacheManager 返回 Cache 实例,各类不一样的 shiro 组件使用这些 Cache 实例来缓存必要的数据。任何实现了 CacheManagerAware 的 shiro 组件将会自动地接收一个配置好的 CacheManager,该 CacheManager 可以用来获取 Cache 实例。
shiro 的 SecurityManager 实现及全部 AuthorizingRealm 实现都实现了 CacheManagerAware 。若是你在 SecurityManager 上设置了 CacheManger,它反过来也会将它设置到实现了 CacheManagerAware 的各类不一样的Realm 上(OO delegation)。
那么为了方便,本节就使用先在比较流行的 ehcache 来作讲解,将spring的cache和shiro的cache结合起来用,经过spring的缓存注解来“缓存数据”,“清除缓存”等操做。
具体配置文件以下,applicationContext.xml:
<!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> <!-- cacheManager,集合spring缓存工厂 --> <property name="cacheManager" ref="cacheManager" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean> <!-- spring对ehcache的缓存工厂支持 --> <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml" /> </bean> <!-- spring对ehcache的缓存管理 --> <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehCacheManagerFactory"></property> </bean> <!-- 使用缓存annotation 配置 --> <cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />
经过以上修改将spring cache和shiro cache整合了起来。完成这个以后先解决一个session集群同步的问题。
在不少web应用中,session共享是很是必要的一件事。而shiro提供了SessionManager给开发人员去管理和维护当前的session。固然,shiro所写的sessionDao 本人认为已经够用了。若是想使用nosql来作存储的话。能够实现SessionManager接口去作你本身的业务逻辑。
提示:
SessionManager(org.apache.shiro.session.SessionManager)知道如何去建立及管理用户 Session 生命周期来为全部环境下的用户提供一个强健的 Session 体验。这在安全框架界是一个独有的特点 shiro 拥有可以在任何环境下本地化管理用户 Session 的能力,即便没有可用的 Web/Servlet 或 EJB 容器,它将会使用它内置的企业级会话管理来提供一样的编程体验。SessionDAO 的存在容许任何数据源可以在持久会话中使用。
SesssionDAO表明SessionManager 执行Session 持久化(CRUD)操做。这容许任何数据存储被插入到会话管理的基础之中。SessionDAO 的权力是你可以实现该接口来与你想要的任何数据存储进行通讯。这意味着你的会话数据能够驻留在内存/缓存中,文件系统,关系数据库或NoSQL 的数据存储,或其余任何你须要的位置。你得控制持久性行为。
EHCache SessionDAO 默认是没有启用的,但若是你不打算实现你本身的SessionDAO,那么强烈地建议你为 shiro 的 SessionManagerment 启用EHCache 支持。EHCache SessionDAO 将会在内存中保存会话,并支持溢出到磁盘,若内存成为制约。这对生产程序确保你在运行时不会随机地“丢失”会话是很是好的。
那么修改applicationContext.xml相关的配置:
<!-- 使用EnterpriseCacheSessionDAO,将session放入到缓存,经过同步配置,将缓存同步到其余集群点上,解决session同步问题。 --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <!-- 活动session缓存名称 --> <property name="activeSessionsCacheName" value="shiroActiveSessionCache" /> </bean> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 使用EnterpriseCacheSessionDAO,解决session同步问题 --> <property name="sessionDAO" ref="sessionDAO" /> </bean> <!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> <!-- cacheManager,集合spring缓存工厂 --> <property name="cacheManager" ref="cacheManager" /> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean> <!-- spring对ehcache的缓存工厂支持 --> <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml" /> </bean> <!-- spring对ehcache的缓存管理 --> <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehCacheManagerFactory"></property> </bean> <!-- 使用缓存annotation 配置 --> <cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />
注意sessionDAO这个bean 里面的属性activeSessionsCacheName就是ehcache的缓存名称。经过该名称能够配置ehcache的缓存性质。
ehcache.xml:
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4446, timeToLive=32" /> <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/> <!-- shiro的活动session缓存名称 --> <cache name="shiroActiveSessionCache" maxElementsInMemory="10000" timeToLiveSeconds="1200" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" properties="replicateAsynchronously=false"/> </cache> </ehcache>
在shiroActiveSessionCache缓存里。集群的配置为同步缓存。做用是subject的getSession可以在全部集群的服务器上共享数据。
完成session共享后在发挥shiro的缓存功能,以1.6 shiro 认证、受权章节为例子,将认证缓存 和 受权缓存 一块儿解决。
认证缓存的用做至关于"热用户"的概念,意思就是说:
当一个用户进行登陆成功后,将该用户记录到缓存中,当下次登陆时,不在去查数据库,而是直接在缓存中获取用户信息,
当缓存满了。而就将缓存里最少使用的用户踢出去。
shiro 的 realm就能实现这个需求,shiro 的 realm 自己就支持缓存。而缓存的踢出规则,ehcache 就能够配置该规则。可是当用户修改信息时,须要将缓存清除。否则下次登陆时,登陆密码用之前旧的密码同样可以登陆,新的密码就不起做用。
具体applicationContext.xml配置以下:
<!-- 使用EnterpriseCacheSessionDAO,将session放入到缓存,经过同步配置,将缓存同步到其余集群点上,解决session同步问题。 --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <!-- 活动session缓存名称 --> <property name="activeSessionsCacheName" value="shiroActiveSessionCache" /> </bean> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 使用EnterpriseCacheSessionDAO,解决session同步问题 --> <property name="sessionDAO" ref="sessionDAO" /> </bean> <!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> <!-- 启用认证缓存,当用户登陆一次后将不在查询数据库来获取用户信息,直接在从缓存获取 --> <property name="authenticationCachingEnabled" value="true" /> <!-- 认证缓存名称 --> <property name="authenticationCacheName" value="shiroAuthenticationCache" /> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> <!-- cacheManager,集合spring缓存工厂 --> <property name="cacheManager" ref="cacheManager" /> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean> <!-- spring对ehcache的缓存工厂支持 --> <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml" /> </bean> <!-- spring对ehcache的缓存管理 --> <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehCacheManagerFactory"></property> </bean> <!-- 使用缓存annotation 配置 --> <cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />
在jdbcAuthenticationRealm这个bean里启用了认证缓存,而这个缓存的名称是shiroAuthenticationCache。
提示:shiro默认不启动认证缓存,若是须要启用,必须在realm里将authenticationCachingEnabled设置成true
ehcache.xml:
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4446, timeToLive=32" /> <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/> <!-- shiro的活动session缓存名称 --> <cache name="shiroActiveSessionCache" maxElementsInMemory="10000" timeToLiveSeconds="1200" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" properties="replicateAsynchronously=false"/> </cache> <!-- shiro认证的缓存名称 --> <cache name="shiroAuthenticationCache" maxElementsInMemory="10000" timeToLiveSeconds="1200" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory properties="replicateAsynchronously=false""/> </cache> </ehcache>
在ehcache.xml中添加了shiroAuthenticationCache缓存。而且memoryStoreEvictionPolicy属性为LRU,LRU就是当缓存满了将“最近最少访问”的缓存踢出。
那么,经过以上配置完成了“热用户”,还有一步就是当修改用户时,将缓存清除,让下次这个用户登陆时,从新去数据库加载新的数据进行认证。
shiro在存储受权用户缓存时,会将用户登陆帐户作键,实体作值的方式进行存储到缓存中。因此,当修改用户时,经过用户的登陆账号,和spring的缓存注解,将该缓存清空。具体代码以下:
@Repository public class UserDao extends BasicHibernateDao<User, String> { /**经过登陆账号获取用户实体**/ public User getUserByUsername(String username) { return findUniqueByProperty("username", username); } /**经过用户实体信息修改用户**/ //当更新后将shiro的认证缓存也更新,保证shiro和当前的用户一致 @CacheEvict(value="shiroAuthenticationCache",key="#entity.getUsername()") public void updateUser(User entity) { update(entity); } /**经过用户实体删除用户**/ //当更新后将shiro的认证缓存也更新,保证shiro和当前的用户一致 @CacheEvict(value="shiroAuthenticationCache",key="#entity.getUsername()") public void deleteUser(User entity) { delete(entity); } }
经过以上代码,当调用updateUser或deletUser方法完成后,spring cache 会将 shiroAuthenticationCache缓存块里key为当前用户的登陆账号的缓存进行清除。
受权缓存的做用大部分是快速获取用户的认证信息,若是存在两个集群点,能够直接使用同步的功能将缓存同步到其余服务器里,当下次访问服务器时,当出现某台服务器没有进行受权工做时,不在进行受权的工做。具体配置以下:
applicationContext.xml:
<!-- 使用EnterpriseCacheSessionDAO,将session放入到缓存,经过同步配置,将缓存同步到其余集群点上,解决session同步问题。 --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <!-- 活动session缓存名称 --> <property name="activeSessionsCacheName" value="shiroActiveSessionCache" /> </bean> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 使用EnterpriseCacheSessionDAO,解决session同步问题 --> <property name="sessionDAO" ref="sessionDAO" /> </bean> <!-- 自定义shiro的realm数据库身份验证 --> <bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm"> <property name="name" value="jdbcAuthentication" /> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5" /> </bean> </property> <!-- 受权缓存名称 --> <property name="authorizationCacheName" value="shiroAuthorizationCache" /> <!-- 启用认证缓存,当用户登陆一次后将不在查询数据库来获取用户信息,直接在从缓存获取 --> <property name="authenticationCachingEnabled" value="true" /> <!-- 认证缓存名称 --> <property name="authenticationCacheName" value="shiroAuthenticationCache" /> </bean> <!-- 使用默认的WebSecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- realm认证和受权,从数据库读取资源 --> <property name="realm" ref="jdbcAuthenticationRealm" /> <!-- cacheManager,集合spring缓存工厂 --> <property name="cacheManager" ref="cacheManager" /> <!-- 考虑到集群,使用DefaultWebSessionManager来作sessionManager --> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 自定义对 shiro的链接约束,结合shiroSecurityFilter实现动态获取资源 --> <bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource"> <!-- 默认的链接配置 --> <property name="filterChainDefinitions"> <value> /login = captchaAuthc /logout = logout /index = perms[security:index] </value> </property> </bean> <!-- 将shiro与spring集合 --> <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <map> <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" /> </map> </property> <!-- shiro的核心安全接口 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登陆时的连接 --> <property name="loginUrl" value="/login" /> <!-- 登录成功后要跳转的链接 --> <property name="successUrl" value="/index" /> <!-- 没有权限要跳转的连接--> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- shiro链接约束配置,在这里使用自定义的动态获取资源类 --> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean> <!-- spring对ehcache的缓存工厂支持 --> <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml" /> </bean> <!-- spring对ehcache的缓存管理 --> <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehCacheManagerFactory"></property> </bean> <!-- 使用缓存annotation 配置 --> <cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />
在applicationContext.xml里的jdbcAuthenticationRealm bean 添加了authorizationCacheName,值为:shiroAuthorizationCache
提示:shiro默认启动受权缓存,若是不想使用受权缓存,将会每次访问到有perms的url都会受权一次。
ehcache.xml:
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4446, timeToLive=32" /> <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/> <!-- shiro的活动session缓存名称 --> <cache name="shiroActiveSessionCache" maxElementsInMemory="10000" timeToLiveSeconds="1200" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" properties="replicateAsynchronously=false"/> </cache> <!-- shiro认证的缓存名称 --> <cache name="shiroAuthenticationCache" maxElementsInMemory="10000" timeToLiveSeconds="1200" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory properties="replicateAsynchronously=false""/> </cache> <!-- shiro受权的缓存名称 --> <cache name="shiroAuthorizationCache" maxElementsInMemory="10000" timeToLiveSeconds="1200"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory properties="replicateAsynchronously=false""/> </cache> </ehcache>
在ehcache中添加shiroAuthorizationCache缓存。将完成受权缓存同步。当修改或删除某些角色时,记得要清楚全部缓存,让用户下次访问时受权一次,否则修改了角色须要重启服务器后才能生效。
@Repository public class GroupDao extends BasicHibernateDao<Group, String> { /**经过用户实体信息修改用户**/ @CacheEvict(value="shiroAuthorizationCache",allEntries=true) public void updateGroup(Group entity) { update(entity); } /**经过用户实体删除用户**/ @CacheEvict(value="shiroAuthorizationCache",allEntries=true) public void deleteGroup(Group entity) { delete(entity); } }
具体的过程在base-framework的showcase的base-curd项目下有例子,若是看不懂。能够根据例子去理解。
dactiv orm 是对持久化层所使用到的框架进行封装,让使用起来不在写如此多的繁琐代码,目前dactiv orm 只支持了 hibernate 4 和 spring data jpa。
dactiv orm 对 hibernate 和 spring data jpa 的修改并很少,经常使用的方法和往常同样使用,除了 hibernate 的 save 方法更名为insert(其实save也是起到了insert的做用,从字面上,insert更加形容了hibernate save方法的做用)其余都和往常同样使用。主要是添加了一些注解和一些简单的执行某些方法先后作了一个拦截处理,以及添加一个灵活的属性查询功能。
在使用 hibernate 时,主要关注几个类:
BasicHibernateDao
HibernateSupportDao
BasicHibernateDao:是对 hibernate 封装的基础类,包含对 hibernate 的基本CURD和其余 hibernate 的查询操做,该类是一个能够支持泛型的CURD类,要是在写dao时的继承体,泛型参数中的 T 为 orm 对象实体类型,PK为实体的主键类型。在类中的 sessionFactory 已经使用了自动写入:
@Autowired(required = false) public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; }
只要用spring配置好sessionFactory就能够继承使用。
applicationContext.xml:
<!-- Hibernate配置 --> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="namingStrategy"> <bean class="org.hibernate.cfg.ImprovedNamingStrategy" /> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.format_sql">${hibernate.format_sql}</prop> </props> </property> <property name="packagesToScan" value="com.github.dactiv.showcase.entity" /> </bean>
UserDao:
public class UserDao extends BasicHibernateDao<User, String> { }
在使用 spring data jpa 时,主要关注BasicJpaRepository这个接口,该接口添加了支持PropertyFilter的方法,能够直接使用,但须要添加配置,要使用到BasicJpaRepository须要在spring data jpa配置文件中对jpa:repositories的factory-class属性添加一个类:org.exitsoft.orm.core.spring.data.jpa.factory.BasicRepositoryFactoryBean:
<jpa:repositories base-package="你的repository包路径" transaction-manager-ref="transactionManager" factory-class="org.exitsoft.orm.core.spring.data.jpa.factory.BasicRepositoryFactoryBean" entity-manager-factory-ref="entityManagerFactory" />
若是以为麻烦,不配置同样可以使用PropertyFilter来作查询操做:
Specifications.get(Lists.newArrayList( PropertyFilters.get("LIKES_loginName", "m"), PropertyFilters.get("EQI_state", "1") ));
该方法会返回一个Specification接口,使用spring data jpa 原生的api findAll方法能够直接使用,执行查询操做:
repository.findAll(Specifications.get(Lists.newArrayList( PropertyFilters.get("LIKES_loginName", "m"), PropertyFilters.get("EQI_state", "1") )));
在 dactiv orm 里,对 hibernate 和 spring data jpa 都扩展了一套查询表达式,是专门用来应付一些比较简单的查询而不用写语句的功能。经过该表达式,dactiv orm 可以解析出最终的查询语句去让 hibernate 或 spring data jpa 去执行,须要使用该表达式,若是是用 hibernate 须要集成 HibernateSupportDao 类,若是使用 spring data jpa 的话须要使用到 Specifications.get() 方法去构造 spring data jpa 的 Specification 后才能执行查询,或者根据 2.2 使用 spring data jpa 配置完成后,集成BasicJpaRepository 接口,里面就提供了支持 PropertyFilter 的查询方法。
该表达式的规则为:<约束名称><属性类型>_<属性名称>,例如如今有个用户实体:
@Entity @Table(name = "TB_ACCOUNT_USER") public class User implements Serializable { private String id;//主键id private String username;//登陆名称 private String password;//登陆密码 private String realname;//真实名称 private Integer state;//状态 private String email;//邮件 //----------GETTER/SETTER-----------// }
经过查询表达式来查询username等于a的用户能够这样写:
hibernate:
public class UserDao extends HibernateSupportDao<User, String>{ }
List<PropertyFilter> filters = Lists.newArrayList( //<约束名称><属性类型>_<属性名称> PropertyFilters.get("EQS_username", "a") ); userDao.findByPropertyFilter(filters);
spring data jpa:
public interface UserRepository extends JpaRepository<User, String>,JpaSpecificationExecutor<User>{ }
userRepository.findAll(Specifications.get(Lists.newArrayList( //<约束名称><属性类型>_<属性名称> PropertyFilters.get("EQS_username", "a") )));
查询username等于a的而且realname等于c的用户能够经过多个条件进行and关系查询:
hibernate:
public class UserDao extends HibernateSupportDao<User, String>{ }
List<PropertyFilter> filters = Lists.newArrayList( //<约束名称><属性类型>_<属性名称> PropertyFilters.get("EQS_username", "a"), PropertyFilters.get("EQS_realname", "c") ); userDao.findByPropertyFilter(filters);
spring data jpa:
public interface UserRepository extends JpaRepository<User, String>,JpaSpecificationExecutor<User>{ }
userRepository.findAll(Specifications.get(Lists.newArrayList( //<约束名称><属性类型>_<属性名称> PropertyFilters.get("EQS_username", "a"), PropertyFilters.get("EQS_realname", "c") )));
了解一些功能后,解释一下 <约束名称><属性类型>_<属性名称> 应该怎么写。
约束名称:约束名称是表达式第一个参数的必须条件,在这里的约束名称是指经过什么条件去作查询,如等于、不等于、包含(in)、大于、小于...等等。
约束名称描述列表:
约束名称 | 描述 |
---|---|
EQ | 等于约束 (from object o where o.value = ?)若是为"null"就是 (from object o where o.value is null) |
NE | 不等于约束 (from object o where o.value <> ?) 若是为"null"就是 (from object o where o.value is not null) |
IN | 包含约束 (from object o where o.value in (?,?,?,?,?)) |
NIN | 不包含约束 (from object o where o.value not in (?,?,?,?,?)) |
GE | 大于等于约束 (from object o where o.value >= ?) |
GT | 大于约束 (from object o where o.value > ?) |
LE | 小于等于约束 ( from object o where o.value <= ?) |
LT | 小于约束 ( from object o where o.value < ?) |
LIKE | 模糊约束 ( from object o where o.value like '%?%') |
LLIKE | 左模糊约束 ( from object o where o.value like '%?') |
RLIKE | 右模糊约束 ( from object o where o.value like '?%') |
属性类型:属性类型是表达式第二个参数的必须条件,表示表达式的属性值是什么类型的值。由于在使用表达式查询时,参数都是String类型的参数,因此必须根据你指定的类型才能自动转为该类型的值,属性类型的描述用一个枚举类来表示,就是 dactiv common 下的 FieldType枚举:
/** * 属性数据类型 * S表明String,I表明Integer,L表明Long, N表明Double, D表明Date,B表明Boolean * * @author calvin * */ public enum FieldType { /** * String */ S(String.class), /** * Integer */ I(Integer.class), /** * Long */ L(Long.class), /** * Double */ N(Double.class), /** * Date */ D(Date.class), /** * Boolean */ B(Boolean.class); //类型Class private Class<?> fieldClass; private FieldType(Class<?> fieldClass) { this.fieldClass = fieldClass; } /** * 获取类型Class * * @return Class */ public Class<?> getValue() { return fieldClass; } }
如,用户对象中实体描述:
@Entity @Table(name = "TB_ACCOUNT_USER") public class User implements Serializable { private String id;//主键id private String username;//登陆名称 private String password;//登陆密码 private String realname;//真实名称 private Integer state;//状态 private String email;//邮件 //----------GETTER/SETTER-----------// }
假如我想查用户状态不等于3的能够写成:
PropertyFilters.get("NEI_state", "3")
假如我想查用户的登陆名称等于a的能够写成:
PropertyFilters.get("EQS_username", "a")
属性名称:属性名称就是实体的属性名,可是要注意的是,经过表达式不能支持别名查询如:
@Entity @Table(name = "TB_ACCOUNT_USER") public class User implements Serializable { private String id;//主键id private String username;//登陆名称 private String password;//登陆密码 private String realname;//真实名称 private Integer state;//状态 private String email;//邮件 private List<Group> groupsList = new ArrayList<Group>();//用户所在的组 //----------GETTER/SETTER-----------// }
想经过PropertyFilters.get("EQS_groupsList.name", "a")就报错,但若是是一对多而且“多”这方的实体在User里面能够经过id查询出来,如:
@Entity @Table(name = "TB_ACCOUNT_USER") public class User implements Serializable { private String id;//主键id private String username;//登陆名称 private String password;//登陆密码 private String realname;//真实名称 private Integer state;//状态 private String email;//邮件 private Group group;//用户所在的组 //----------GETTER/SETTER-----------// }
PropertyFilters.get("EQS_group.id", "1")
若是想使用别名的查询,能够经过重写的方法来实现该功能,以 hibernate 为例子,HibernateSupportDao的表达式查询方法其实都是在用QBC形式的查询,就是Criteria。在该类中都是经过:createCriteria建立Criteria的,因此。在UserDao中重写该方法就能够实现别名查询了,如:
public class UserDao extends HibernateSupportDao<User, String>{ protected Criteria createCriteria(List<PropertyFilter> filters,Order ...orders) { Criteria criteria = super.createCriteria(filters, orders); criteria.createAlias("groupsList", "gl"); return criteria; } }
PropertyFilters.get("EQS_groupsList.name", "mm")
表达式and与or的多值写法:有时候会经过表达式去查询某个属性等于多个值或者多个属性等于某个值的须要,在某个属性等于多个值时,若是用and查询的话把值用逗号","分割,若是用or查询的话把值用横杠"|"分割。如:
and:
PropertyFilters.get("EQS_username", "1,2,3")
or:
PropertyFilters.get("EQS_username", "1|2|3")
在须要查询多个属性等于某个值时,使用OR分隔符隔开这些属性:
PropertyFilters.get("EQS_username_OR_email", "xxx@xxx.xx");
多条件以及分页查询,可能每一个项目中都会使用,可是查询条件变幻无穷,当某时客户要求添加多一个查询条件时,繁琐的工做会不少,但使用 PropertyFilter 会为你减小一些复制粘贴的动做。
以用户实体类例:
@Entity @Table(name = "TB_ACCOUNT_USER") public class User implements Serializable { private String id;//主键id private String username;//登陆名称 private String password;//登陆密码 private String realname;//真实名称 private Integer state;//状态 private String email;//邮件 //----------GETTER/SETTER-----------// }
先在的查询表单以下:
<form id="search_form" action="account/user/view" method="post"> <label for="filter_RLIKES_username"> 登陆账号: </label> <input type="text" id="filter_RLIKES_username" name="filter_RLIKES_username" /> <label for="filter_RLIKES_realname"> 真实姓名: </label> <input type="text" id="filter_RLIKES_realname" name="filter_RLIKES_realname" /> <label for="filter_RLIKES_email"> 电子邮件: </label> <input type="text" id="filter_RLIKES_email" name="filter_RLIKES_email" /> <label for="filter_EQS_state"> 状态: </label> </form>
注意看每一个input的name属性,在input的name里经过filter_作前缀加查询表达式,当表单提交过来时经过一下代码完成查询:
public class UserDao extends HibernateSupportDao<User, String>{ }
@RequestMapping("view") public Page<User> view(PageRequest pageRequest,HttpServletRequest request) { List<PropertyFilter> filters = PropertyFilters.get(request, true); if (!pageRequest.isOrderBySetted()) { pageRequest.setOrderBy("id"); pageRequest.setOrderDir(Sort.DESC); } return userDao.findPage(pageRequest, filters); }
当客户在某时想添加一个经过状态查询时,只须要在表单中添加多一个select便可完成查询。
<form id="search_form" action="account/user/view" method="post"> <...> <select name="filter_EQI_state" id="filter_EQS_state" size="25"> <option value=""> 所有 </option> <option value="1"> 禁用 </option> <option value="2"> 启用 </option> </select> </form>
若是你在项目开发时以为表达式里面的约束名称不够用,能够对表达式作扩展处理。扩展约束名称时 spring data jpa 和 hibernate 所关注的类不一样。
要扩展 hibernate 查询表达式的约束名主要关注的类有 HibernateRestrictionBuilder, CriterionBuilder,CriterionSingleValueSupport 以及 CriterionMultipleValueSupport。
HibernateRestrictionBuilder:HibernateRestrictionBuilder 是装载全部 CriterionBuilder 实现类的包装类,该类有一块静态局域。去初始化全部的约束条件。并提供两个方法去建立 Hibernate 的 Criterion,该类是 HibernateSupportDao 查询表达式查询的关键类。全部经过条件建立的 Criterion 都是经过该类建立。
CriterionBuilder:CriterionBuilder是一个构造Hibernate Criterion的类,该类有一个方法专门提供根据 PropertyFilter 该如何建立 hibernate 的 Criterion,而该类有一个抽象类实现了部分方法,就是 CriterionSingleValueSupport,具体 CriterionBuilder 的接口以下:
public interface CriterionBuilder { /** * 获取Hibernate的约束标准 * * @param filter 属性过滤器 * * @return {@link Criterion} * */ public Criterion build(PropertyFilter filter); /** * 获取Criterion标准的约束名称 * * @return String */ public String getRestrictionName(); /** * 获取Hibernate的约束标准 * * @param propertyName 属性名 * @param value 值 * * @return {@link Criterion} * */ public Criterion build(String propertyName,Object value); }
CriterionSingleValueSupport:该类是CriterionBuilder的子类,实现了public Criterion build(PropertyFilter filter)实现体主要是对PropertyFilter的值模型作处理。而且逐个循环调用public Criterion build(String propertyName,Object value)方法给 CriterionSingleValueSupport 实现体作处理。
public abstract class CriterionSingleValueSupport implements CriterionBuilder{ //or值分隔符 private String orValueSeparator = "|"; //and值分隔符 private String andValueSeparator = ","; public CriterionSingleValueSupport() { } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.hibernate.CriterionBuilder#build(com.github.dactiv.orm.core.PropertyFilter) */ public Criterion build(PropertyFilter filter) { String matchValue = filter.getMatchValue(); Class<?> FieldType = filter.getFieldType(); MatchValue matchValueModel = getMatchValue(matchValue, FieldType); Junction criterion = null; if (matchValueModel.hasOrOperate()) { criterion = Restrictions.disjunction(); } else { criterion = Restrictions.conjunction(); } for (Object value : matchValueModel.getValues()) { if (filter.hasMultiplePropertyNames()) { List<Criterion> disjunction = new ArrayList<Criterion>(); for (String propertyName:filter.getPropertyNames()) { disjunction.add(build(propertyName,value)); } criterion.add(Restrictions.or(disjunction.toArray(new Criterion[disjunction.size()]))); } else { criterion.add(build(filter.getSinglePropertyName(),value)); } } return criterion; } /** * 获取值对比模型 * * @param matchValue 值 * @param FieldType 值类型 * * @return {@link MatchValue} */ public MatchValue getMatchValue(String matchValue,Class<?> FieldType) { return MatchValue.createMatchValueModel(matchValue, FieldType,andValueSeparator,orValueSeparator); } /** * 获取and值分隔符 * * @return String */ public String getAndValueSeparator() { return andValueSeparator; } /** * 设置and值分隔符 * @param andValueSeparator and值分隔符 */ public void setAndValueSeparator(String andValueSeparator) { this.andValueSeparator = andValueSeparator; } }
CriterionMultipleValueSupport:该类是 CriterionSingleValueSupport 的子类。重写了CriterionSingleValueSupport类的public Criterion build(PropertyFilter filter) 和 public Criterion build(String propertyName, Object value) 方法。而且添加了一个抽象方法 public abstract Criterion buildRestriction(String propertyName,Object[] values) 。该类主要做用是在多值的状况不逐个循环,而是将全部的参数组合成一个数组传递给抽象方法buildRestriction(String propertyName,Object[] values)中。这种状况在in或not in约束中就用获得。
public abstract class CriterionMultipleValueSupport extends CriterionSingleValueSupport{ /** * 将获得值与指定分割符号,分割,获得数组 * * @param value 值 * @param type 值类型 * * @return Object */ public Object convertMatchValue(String value, Class<?> type) { Assert.notNull(value,"值不能为空"); String[] result = StringUtils.splitByWholeSeparator(value, getAndValueSeparator()); return ConvertUtils.convertToObject(result,type); } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.hibernate.restriction.CriterionSingleValueSupport#build(com.github.dactiv.orm.core.PropertyFilter) */ public Criterion build(PropertyFilter filter) { Object value = convertMatchValue(filter.getMatchValue(), filter.getFieldType()); Criterion criterion = null; if (filter.hasMultiplePropertyNames()) { Disjunction disjunction = Restrictions.disjunction(); for (String propertyName:filter.getPropertyNames()) { disjunction.add(build(propertyName,value)); } criterion = disjunction; } else { criterion = build(filter.getSinglePropertyName(),value); } return criterion; } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.hibernate.CriterionBuilder#build(java.lang.String, java.lang.Object) */ public Criterion build(String propertyName, Object value) { return buildRestriction(propertyName, (Object[])value); } /** * * 获取Hibernate的约束标准 * * @param propertyName 属性名 * @param values 值 * * @return {@link Criterion} */ public abstract Criterion buildRestriction(String propertyName,Object[] values); }
了解完以上几个类。那么假设如今有个需求。要写一个模糊非约束 (from object o where o.value not like '%?%')来判断一些值,能够经过继承CriterionSingleValueSupport类,实现public Criterion build(String propertyName, Object value),如:
/** * 模糊非约束 ( from object o where o.value not like '%?%') RestrictionName:NLIKE * 表达式:NLIKE_属性类型_属性名称[|属性名称...] * * @author vincent * */ public class NlikeRestriction extends CriterionSingleValueSupport{ public final static String RestrictionName = "NLIKE"; @Override public String getRestrictionName() { return RestrictionName; } @Override public Criterion build(String propertyName, Object value) { return Restrictions.not(Restrictions.like(propertyName, value.toString(), MatchMode.ANYWHERE)); } }
经过某种方式(如spring的InitializingBean,或serlvet)将该类添加到HibernateRestrictionBuilder的CriterionBuilder中。就可使用约束名了。
CriterionBuilder nlikeRestriction= new NlikeRestriction(); HibernateRestrictionBuilder.getCriterionMap().put(nlikeRestriction.getRestrictionName(), nlikeRestriction);
若是使用 spring data jpa 作 orm 支持时,要扩展查询表达式的约束名主要关注的类有 JpaRestrictionBuilder, PredicateBuilder,PredicateSingleValueSupport 以及 PredicateMultipleValueSupport。
JpaRestrictionBuilder:JpaRestrictionBuilder 是装载全部 PredicateBuilder 实现的包装类,该类有一块静态局域。去初始化全部的约束条件。并提供两个方法去建立 jpa 的 Predicate,该类是 BasicJpaRepository 查询表达式查询的关键类。全部经过 PropertyFilter 建立的 Predicate 都是经过该类建立。
PredicateBuilder:PredicateBuilder 是一个构造 jpa Predicate 的类,该类有一个方法专门提供根据 PropertyFilter 该如何建立 jpa 的 Predicate,而该类有一个抽象类实现了部分方法,就是 PredicateSingleValueSupport,具体 PredicateBuilder 的接口以下:
public interface PredicateBuilder { /** * 获取Jpa的约束标准 * * @param filter 属性过滤器 * @param entity jpa查询绑定载体 * * @return {@link Predicate} * */ public Predicate build(PropertyFilter filter,SpecificationEntity entity); /** * 获取Predicate标准的约束名称 * * @return String */ public String getRestrictionName(); /** * 获取Jpa的约束标准 * * @param propertyName 属性名 * @param value 值 * @param entity jpa查询绑定载体 * * @return {@link Predicate} * */ public Predicate build(String propertyName, Object value,SpecificationEntity entity); }
PredicateSingleValueSupport:该类是PredicateBuilder的子类,实现了 public Predicate build(PropertyFilter filter,SpecificationEntity entity) 方法,实现体主要是对 PropertyFilter 的值模型作处理。而且逐个循环调用 public Predicate build(String propertyName, Object value,SpecificationEntity entity) 方法给实现体作处理:
public abstract class PredicateSingleValueSupport implements PredicateBuilder{ //or值分隔符 private String orValueSeparator = "|"; //and值分隔符 private String andValueSeparator = ","; public PredicateSingleValueSupport() { } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.spring.data.jpa.PredicateBuilder#build(com.github.dactiv.orm.core.PropertyFilter, javax.persistence.criteria.Root, javax.persistence.criteria.CriteriaQuery, javax.persistence.criteria.CriteriaBuilder) */ public Predicate build(PropertyFilter filter,SpecificationEntity entity) { String matchValue = filter.getMatchValue(); Class<?> FieldType = filter.getFieldType(); MatchValue matchValueModel = getMatchValue(matchValue, FieldType); Predicate predicate = null; if (matchValueModel.hasOrOperate()) { predicate = entity.getBuilder().disjunction(); } else { predicate = entity.getBuilder().conjunction(); } for (Object value : matchValueModel.getValues()) { if (filter.hasMultiplePropertyNames()) { for (String propertyName:filter.getPropertyNames()) { predicate.getExpressions().add(build(propertyName, value, entity)); } } else { predicate.getExpressions().add(build(filter.getSinglePropertyName(), value, entity)); } } return predicate; } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.spring.data.jpa.PredicateBuilder#build(java.lang.String, java.lang.Object, com.github.dactiv.orm.core.spring.data.jpa.JpaBuilderModel) */ public Predicate build(String propertyName, Object value,SpecificationEntity entity) { return build(Specifications.getPath(propertyName, entity.getRoot()),value,entity.getBuilder()); } /** * * 获取Jpa的约束标准 * * @param expression 属性路径表达式 * @param value 值 * @param builder CriteriaBuilder * * @return {@link Predicate} */ public abstract Predicate build(Path<?> expression,Object value,CriteriaBuilder builder); /** * 获取值对比模型 * * @param matchValue 值 * @param FieldType 值类型 * * @return {@link MatchValue} */ public MatchValue getMatchValue(String matchValue,Class<?> FieldType) { return MatchValue.createMatchValueModel(matchValue, FieldType,andValueSeparator,orValueSeparator); } /** * 获取and值分隔符 * * @return String */ public String getAndValueSeparator() { return andValueSeparator; } /** * 设置and值分隔符 * @param andValueSeparator and值分隔符 */ public void setAndValueSeparator(String andValueSeparator) { this.andValueSeparator = andValueSeparator; } }
PredicateMultipleValueSupport:该类是 PredicateSingleValueSupport 的子类。重写了 PredicateSingleValueSupport 类的public Predicate build(PropertyFilter filter, SpecificationEntity entity) 和 public Predicate build(Path<?> expression, Object value,CriteriaBuilder builder)。而且添加了一个抽象方法 public abstract Predicate buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder)。该类主要做用是在多值的状况不逐个循环,而是所有都将参数组合成一个数组传递给抽象方法 buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder) 中。这种状况在in或not in约束中就用获得。
public abstract class PredicateMultipleValueSupport extends PredicateSingleValueSupport{ /** * 将获得值与指定分割符号,分割,获得数组 * * @param value 值 * @param type 值类型 * * @return Object */ public Object convertMatchValue(String value, Class<?> type) { Assert.notNull(value,"值不能为空"); String[] result = StringUtils.splitByWholeSeparator(value, getAndValueSeparator()); return ConvertUtils.convertToObject(result,type); } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.spring.data.jpa.restriction.PredicateSingleValueSupport#build(com.github.dactiv.orm.core.PropertyFilter, com.github.dactiv.orm.core.spring.data.jpa.JpaBuilderModel) */ public Predicate build(PropertyFilter filter, SpecificationEntity entity) { Object value = convertMatchValue(filter.getMatchValue(), filter.getFieldType()); Predicate predicate = null; if (filter.hasMultiplePropertyNames()) { Predicate orDisjunction = entity.getBuilder().disjunction(); for (String propertyName:filter.getPropertyNames()) { orDisjunction.getExpressions().add(build(propertyName,value,entity)); } predicate = orDisjunction; } else { predicate = build(filter.getSinglePropertyName(),value,entity); } return predicate; } /* * (non-Javadoc) * @see com.github.dactiv.orm.core.spring.data.jpa.restriction.PredicateSingleValueSupport#build(javax.persistence.criteria.Path, java.lang.Object, javax.persistence.criteria.CriteriaBuilder) */ public Predicate build(Path<?> expression, Object value,CriteriaBuilder builder) { return buildRestriction(expression,(Object[])value,builder); } /** * 获取Jpa的约束标准 * * @param expression root路径 * @param values 值 * @param builder CriteriaBuilder * * @return {@link Predicate} */ public abstract Predicate buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder); }
了解完以上几个类。那么假设如今有个需求。要写一个模糊约束 (from object o where o.value like '%?%')来判断一些值,能够经过继承PredicateSingleValueSupport类,实现build(Path<?> expression,Object value,CriteriaBuilder builder),如:
/** * 模糊约束 ( from object o where o.value like '%?%') RestrictionName:LIKE * <p> * 表达式:LIKE属性类型_属性名称[_OR_属性名称...] * </p> * * @author vincent * */ public class LikeRestriction extends PredicateSingleValueSupport{ public String getRestrictionName() { return RestrictionNames.LIKE; } @SuppressWarnings({ "rawtypes", "unchecked" }) public Predicate build(Path expression, Object value,CriteriaBuilder builder) { return builder.like(expression, "%" + value + "%"); } }
经过某种方式(如spring的InitializingBean,或serlvet)将该类添加到JpaRestrictionBuilder的PredicateBuilder中。就可使用约束名了。
PredicateBuilder nlikeRestriction= new NlikeRestriction(); JpaRestrictionBuilder.getCriterionMap().put(nlikeRestriction.getRestrictionName(), nlikeRestriction);
对于 hibernate 和 spring data jpa 的约束名在base-framework的 dactiv orm 都会有例子。若是不明白。能够看例子理解。
在 dactiv orm 中,扩展一些比较经常使用的注解,如:状态删除、防伪安全码等。当须要执行某些特定的需求时,无需在写繁琐的代码。而是直接加上注解便可。
状态删除:在大部分的业务系统中有某些表对于 delete 操做都有这么一个需求,就是不物理删除,只把状态改为修改状态。dactiv orm提供了这种功能。只要在实体类加上 @StateDelete 注解后调用 orm 框架的删除方法,将会改变实体某个字段的值作已删除的状态值。例若有一个User实体,在删除时要改变 state 值为3,看成已删除记录。能够这样写实体:
@Entity @Table(name = "TB_ACCOUNT_USER") @StateDelete(propertyName = "state", value = "3") public class User implements Serializable { //主键id private Integer id; //状态 private Integer state; //----------GETTER/SETTER-----------// }
hibernate:
public class UserDao extends BasicHibernateDao<User, Integer> { }
调用 deleteByEntity 方法后会生成如下sql语句:
User user = userDao.load(1); userDao.deleteByEntity(user); sql:update tb_account_user set state = ? where id = ?
spring data jpa:
public interface UserDao extends BasicJpaRepository<User, Integer> { }
调用 delete 方法后会生成如下sql语句:
userDao.delete(1); sql:update tb_account_user set state = ? where id = ?
若是 orm 框架为 spring data jpa 而且要用到这个功能,记得把.BasicRepositoryFactoryBean类配置到jpa:repositories标签的factory-class属性中。
安全码:在某些业务系统中,有某些表涉及到钱的敏感字段时,为了防止数据被串改,提到了安全码功能。就是在表里面加一个字段,该字段的值是表中的主键id和其余比较敏感的字段值并合起来加密存储到安全码字段的值。当下次更新时记录时,会先用id获取数据库的记录,并再次将数据加密与安全吗字段作对比,若是安全码不相同。表示该记录有问题。将丢出异常。
dactiv orm 提供了这种功能。只要在实体类加上 @SecurityCode 注解后调用 orm 框架的update方法时,将根据id获取一次记录并加密去对比安全吗。若是相同执行 update 操做,不然丢出 SecurityCodeNotEqualException 异常。
记录用上面的用户实体
@Entity @Table(name = "TB_ACCOUNT_USER") @SecurityCode(value="securityCode",properties={"money"}) @StateDelete(propertyName = "state", value = "3") public class User implements Serializable { //主键id private Integer id; //状态 private Integer state; //帐户余额 private Double money; //安全吗 private String securityCode; //----------GETTER/SETTER-----------// }
经过以上代码,在更新时,会将 id + money 的值链接起来,并使用md5加密的方式加密一个值,赋值到 securityCode 中:
hibernate:
public class UserDao extends BasicHibernateDao<User, Integer> { }
User user = userDao.load(1); user.setMoney(1000000.00); userDao.update(user); //or userDao.save(user);
spring data jpa:
public interface UserDao extends BasicJpaRepository<User, Integer> { }
User user = userDao.findOne(1); user.setMoney(1000000.00); userDao.save(user);
若是orm 框架为 spring data jpa 而且要用到这个功能,记得把.BasicRepositoryFactoryBean类配置到jpa:repositories标签的factory-class属性中。
树形实体:某些时候,在建立自身关联一对多时,每每会出现 n + 1 的问题。就是在遍历树时,获取子节点永远都会去读多一次数据库。特别是用到 orm 框架时,lazy功能会主要你调用到该方法,java代理就去会执行获取属性的操做,同时也产生了数据库的访问。为了不这些 n + 1 不少人想到了在实体中加多一个属性去记录该节点是否包含子节点,若是包含就去读取,不然将不读取。如如下实体:
@Entity @Table(name = "TB_ACCOUNT_RESOURCE") public class Resource implements Serializable{ //主键id private Integer id; //名称 private String name; //父类 private Resource parent; //是否包含叶子节点 private Boolean leaf = Boolean.FALSE; //子类 private List<Resource> children = new ArrayList<Resource>(); }
经过该实体,能够经过leaf字段是判断是否包含子节点,当等于true时表示有子节点,能够调用getChildren()方法去读取子节点信息。但如今问题是。要管理这个 leaf 字段彷佛每次插入、保存、删除操做都会涉及到该值的修改问题。如:当前数据没有子节点,但添加了一个子节点进去后。当前数据的 leaf 字段要改为true。又或者:当前数据存在子节点,但子节点删除完时,要更新 leaf 字段成 false。又或者,当钱数据存在子节点,但 update 时移除了子节点,要更新 leaf 字段成 false。这些状况下只要调用到 update、 insert、 delete方法都要去更新一次数据。
dactiv orm 提供了这种功能。只要在实体类加上 @TreeEntity 注解后调用增、删、改操做会帮你维护该leaf字段。
@Entity @TreeEntity @Table(name = "TB_ACCOUNT_RESOURCE") public class Resource implements Serializable{ //主键id private Integer id; //名称 private String name; //父类 private Resource parent; //是否包含叶子节点 private Boolean leaf = Boolean.FALSE; //子类 private List<Resource> children = new ArrayList<Resource>(); }
TreeEntity注解里面有一个属性为refreshHql,该属性是一个hql语句,在调用增、删、该方法时,会使用该语句去查一次数据,将全部的数据状态为:没子节点但leaf又等于true的数据加载出来,并设置这些数据的 leaf 属性为 false 值。
TreeEntity注解源码:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TreeEntity { /** * 是否包含叶子的标记属性名 * * @return String */ public String leafProperty() default "leaf"; /** * tree实体的父类属性名 * * @return String */ public String parentProperty() default "parent"; /** * 刷新的hql语句 * * @return String */ public String refreshHql() default "from {0} tree " + "where tree.{1} = {2} and (" + " select count(c) from {0} c " + " where c.{3}.{4} = tree.{4} " + ") = {5}"; /** * 是否包含叶子的标记属性类型 * * @return Class */ public Class<?> leafClass() default Boolean.class; /** * 若是是包含叶子节点须要设置的值 * * @return String */ public String leafValue() default "1"; /** * 若是不是包含叶子节点须要设置的值 * * @return String */ public String unleafValue() default "0"; }
若是 orm 框架为 spring data jpa 而且要用到这个功能,记得把.BasicRepositoryFactoryBean类配置到jpa:repositories标签的factory-class属性中。
对于 dactiv orm 使用所讲的一切在base-framework的dactiv-orm项目中都有例子,若是不懂。能够看例子去理解。