之前一直使用spring security来做安全管理,感觉配置稍微有点复杂,于是尝试了下shiro,感觉的确简单不少。记录下配置和实现过程。
因为还是spring的底子,所以用的shiro-spring,首先用maven把相关包弄下来
dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency>
我用的版本是1.2.2
然后开始增加shiro的配置文件,xml里增加了shiro的配置引入,同时增加相应的filter
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:/application-root.xml, classpath*:/application-shiro.xml </param-value> </context-param> <!-- shiro security filter --> <filter> <!-- 这里的filter-name要和spring的applicationContext-shiro.xml里的 org.apache.shiro.spring.web.ShiroFilterFactoryBean的bean name相同 --> <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>
下面是shiro的配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd" default-lazy-init="true"> <description>Shiro安全配置</description> <bean id="chainDefinitionSectionMetaSource" class="xxx.xxx.ChainDefinitionSectionMetaSource"> <property name="filterChainDefinitions"> <value> /login = authc /logout = logout /admin/** = roles[admin] /static/** = anon /eventSurvey/** = anon /notice/** = anon /** = authc </value> </property> </bean> <!-- Shiro's main business-tier object for web-enabled applications --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="shiroDbRealm" /> </bean> <!-- 項目自定义的Realm, 所有accountService依赖的dao都需要用depends-on声明 --> <bean id="shiroDbRealm" class="xxx.xxx.ShiroDbRealm"> <property name="credentialsMatcher" ref="flameCredentialsMatcher" /> </bean> <bean id="flameCredentialsMatcher" class="xxx.xxx.FlameCredentialsMatcher"> </bean> <!-- Shiro Filter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login" /> <property name="successUrl" value="/" /> <property name="unauthorizedUrl" value="/authError" /> <property name="filters"> <map> <entry key="authc"> <bean class="xxx.xxx.MyFormAuthenticationFilter"></bean> </entry> <entry key="roles"> <bean class="org.apache.shiro.web.filter.authz.RolesAuthorizationFilter"></bean> </entry> </map> </property> <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" /> </bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>
这里需要说明几个配置
1)shiro本身的过滤配置已经很好了,但我们要求可从数据库加载过滤配置,所以增加了chainDefinitionSectionMetaSource的实现,用来同时从配置文件和数据库加载过滤信息,实现如下
public class ChainDefinitionSectionMetaSource implements FactoryBean<Ini.Section>{ @Autowired private ResourceDao resourceDao; private String filterChainDefinitions; public Section getObject() throws BeansException { //获取所有Resource List<Resource> list = resourceDao.findAll(); Ini ini = new Ini(); //加载默认的url ini.load(filterChainDefinitions); Ini.Section section = ini.getSection(Ini.DEFAULT_SECTION_NAME); //循环Resource的url,逐个添加到section中。section就是filterChainDefinitionMap, //里面的键就是链接URL,值就是存在什么条件才能访问该链接 for (Resource resource : list) { //如果不为空值添加到section中 if(StringUtils.hasText(resource.getUrl()) && StringUtils.hasText(resource.getPerms())) { section.put(resource.getUrl(), resource.getPerms()); } } return section; } /** * 通过filterChainDefinitions对默认的url过滤定义 * * @param filterChainDefinitions 默认的url过滤定义 */ public void setFilterChainDefinitions(String filterChainDefinitions) { this.filterChainDefinitions = filterChainDefinitions; } public Class<?> getObjectType() { return this.getClass(); } public boolean isSingleton() { return false; } }
这里的Resource是从数据库得到的数据,其他跟xml配置差不多,就是key-value一样的。
2)shiroDbRealm,负责根据自己的业务抓取用户信息,主要是为了安全认证用
public class ShiroDbRealm extends AuthorizingRealm{ @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { UserInfo user = (UserInfo) principals.getPrimaryPrincipal(); List<String> userAuths = user.getAuthList(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(userAuths); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; UserInfo user = userService.findUserById(token.getUsername()); if (user != null) { String authPassword = user.getPassword(); return new SimpleAuthenticationInfo(user, authPassword,getName()); } else { return null; } } }
3)credentialsMatcher,这里是处理密码匹配用的,通常密码都是加密的,用户前台输入的密码需要自己加密算法来匹配
/** * 处理密码加密 * @author lee * */ public class FlameCredentialsMatcher extends SimpleCredentialsMatcher{ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String userId = token.getPrincipal().toString(); char[] ps = (char[]) token.getCredentials(); StringBuffer sb = new StringBuffer(); for(char p : ps){ sb.append(p); } String tokenMd5Pw = Encryption.encrypt(userId,sb.toString()); return equals(tokenMd5Pw, info.getCredentials()); } }
上面的Encryption.encrypt(userId,sb.toString())是一个加密算法处理,这个可以使用shiro的加密API,也可以自己实现。info是shiroDbRealm取回的用户信息,token是前台传过来的信息。
4)对于shiro的过滤链我使用了两个简单的,一个是处理登录和权限校验的authc,一个是处理角色校验的roles,这里需要说下的是如果在filter中不增加roles,那么配置/admin/** = roles[admin]这种就不会生效。对于authc因为要做是否是ajax的验证,所以做了自己的实现封装
public class MyFormAuthenticationFilter extends FormAuthenticationFilter{ private static final Logger log = LoggerFactory.getLogger(MyFormAuthenticationFilter.class); /* * 主要是针对登入成功的处理方法。对于请求头是AJAX的之间返回JSON字符串。 */ @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { if (!isAjax(request)) {// 不是ajax请求 issueSuccessRedirect(request, response); } else { response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter out = response.getWriter(); out.println("{\"success\":true,\"message\":\"登入成功\"}"); out.flush(); out.close(); } return false; } /** * 主要是处理登入失败的方法 */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { if (!isAjax(request)) {// 不是ajax请求 setFailureAttribute(request, e); return true; } try { response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter out = response.getWriter(); String message = e.getClass().getSimpleName(); if ("IncorrectCredentialsException".equals(message)) { out.println("{\"success\":false,\"message\":\"密码错误\"}"); } else if ("UnknownAccountException".equals(message)) { out.println("{\"success\":false,\"message\":\"账号不存在\"}"); } else if ("LockedAccountException".equals(message)) { out.println("{\"success\":false,\"message\":\"账号被锁定\"}"); } else { out.println("{\"success\":false,message:\"未知错误\"}"); } out.flush(); out.close(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } return false; } /** * 所有请求都会经过的方法。 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { log.info("权限验证"); if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } if (!isAjax(request)) {// 不是ajax请求 saveRequestAndRedirectToLogin(request, response); } else { response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter out = response.getWriter(); out.println("{\"success\":false,\"message\":\"login\"}"); out.flush(); out.close(); } return false; } } private boolean isAjax(ServletRequest request){ HttpServletRequest httpServletRequest = (HttpServletRequest) request; return "XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("X-Requested-With")); } }
都配置完成,shiro就跑起来了