之前一直使用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就跑起来了