[每写一次博客,都是对自己学习的一个总结,也希望能帮到遇到同样问题的人]
首先说一下why apache shiro
最近在学着从前端到后端做一个网站来玩,那要搭建一个网站,用户和权限系统肯定是很重要的了。首先是权限系统,可以自己实现一个简单的控制,也可以使用开源的框架。由于自己是学习阶段,所以还是参考开源的框架比较好。网上搜索过后,目前用的比较多的两个开源框架就是apache shiro和spring-security。 Spring-security的功能比较强大,但是配置比较麻烦,显然,这两者是矛盾的。shiro配置比较简单,虽然功能没那么强大,但是也提供了很多可以自己定制的接口。经过两者的对比,决定还是选用shiro。2016-05-07
Version 1.0
这是第一阶段,只实现了基本的用户验证功能。
首先,pom配置,这个就不用多说了。
<span style="font-family: Arial, Helvetica, sans-serif;"> <dependency></span>
<groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
接下来Spring配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="lifecyleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean id="shiroDbRealm" class="org.rikey.web.biz.shiro.ShiroDBRealm"> // 配置shiro的权限管理器为DB数据库的权限管理器,<span style="color:#ff0000;">需要自己实现</span>,后面会讲 <property name="credentialsMatcher"> // 配置密码校验器 <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="SHA-256"/> <property name="storedCredentialsHexEncoded" value="false"/> </bean> </property> </bean> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> // security manager <property name="realm" ref="shiroDbRealm"/> <property name="cacheManager" ref="cacheManager"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> // 权限过滤器,很多都见名知意了 <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.htm"/> <property name="unauthorizedUrl" value="/403.htm"/> <property name="filterChainDefinitions"> <value> /login*=anon // anon 匿名, authc 需要鉴权 /dologin*=anon /register.htm=anon /doregister=anon /logout*=anon /css/**=anon /js/**=anon /user/**=anon /**=authc </value> </property> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/> // 这两个照着配就行了 <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> </beans>
第三步,web.xml,这里面还需要配置
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecyle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
配置shiro的filter,对所有的uri进行过滤
第四步:接下来,就需要编码了。
1.注意到前面使用了一个ShiroDBRealm,这个是我们自己根据shiro的接口实现的一个基于数据库的鉴权授权类。
2.另外,shiro只提供了鉴权授权,对于用户注册的时候,密码的保存机制,必须和ShiroDBRealm中用到的密码加密机制一样。(当然,你可以保存明文,就不需要特殊处理用户注册时候的密码了)
1. ShiroDBRealm
package org.rikey.web.biz.shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.rikey.web.biz.shiro.result.UserLoginResult; import org.rikey.web.dao.UserDao; import org.rikey.web.domain.User; import javax.annotation.Resource; public class ShiroDBRealm extends AuthorizingRealm { @Resource(name = "userDao") private UserDao userDao; private static final String REALM_NAME = "shiroDbRealm"; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String userName = (String)super.getAvailablePrincipal(principals); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Subject currentUser = SecurityUtils.getSubject(); if (null != currentUser) { Session session = currentUser.getSession(); if (session != null) { UserLoginResult user = (UserLoginResult)session.getAttribute("currentUser"); authorizationInfo.addStringPermissions(user.getPermissions()); return authorizationInfo; } } return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token; String userName = usernamePasswordToken.getUsername(); User user = userDao.queryUser(userName); if (user == null || user.getPassword() == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), REALM_NAME); return authenticationInfo; } }
doGetAuthorizationInfo方法先不看,这个是用来获取授权信息的(用户对各个uri是否有权限的信息),这次还没涉及到这个。
重点看doGetAuthencationInfo方法,调用方(shiro框架里的方法来调用)会传入一个token,里面会包含用户名,我们在这个方法中要做的事就是根据框架传进来的用户名,到数据库中去获取该用户的密码,salt值,然后封装成一个SimpleAuthenticationInfo对象,传出去就行了,其他事情就交给我们前面在spring中配置的HashedCredentialsMather来做了。(其实到这里,你可以发现了,shiro里面很多东西,我们都可以自己实现的,比如这个CredentialsMatcher,可以实现我们自己的密码校验逻辑,可以在里面检查密码错误次数等)。
第5步:
有了前面的基础工作,现在就可以在自己的web项目中使用shiro来进行鉴权了。
在处理登录请求的controller中:
@RequestMapping(value = {"/dologin.htm"}, method = RequestMethod.POST) public String doLogin(String userName, String password,Boolean remindMe, Model model) { UsernamePasswordToken token = new UsernamePasswordToken(userName, password); remindMe = remindMe == null ? false : true; token.setRememberMe(remindMe); Subject subject = SecurityUtils.getSubject(); String msg; try { subject.login(token); if (subject.isAuthenticated()) { return "redirect:/"; } else { model.addAttribute("msg", "用户名或密码错误"); return "login"; } } catch (IncorrectCredentialsException e) { msg = "登录密码错误. Password for account " + token.getPrincipal() + " was incorrect."; model.addAttribute("msg", msg); System.out.println(msg); } catch (ExcessiveAttemptsException e) { msg = "登录失败次数过多"; model.addAttribute("msg", msg); System.out.println(msg); } catch (LockedAccountException e) { msg = "帐号已被锁定. The account for username " + token.getPrincipal() + " was locked."; model.addAttribute("msg", msg); System.out.println(msg); } catch (DisabledAccountException e) { msg = "帐号已被禁用. The account for username " + token.getPrincipal() + " was disabled."; model.addAttribute("msg", msg); System.out.println(msg); } catch (ExpiredCredentialsException e) { msg = "帐号已过期. the account for username " + token.getPrincipal() + " was expired."; model.addAttribute("msg", msg); System.out.println(msg); } catch (UnknownAccountException e) { msg = "帐号不存在. There is no user with username of " + token.getPrincipal(); model.addAttribute("msg", msg); System.out.println(msg); } catch (UnauthorizedException e) { msg = "您没有得到相应的授权!" + e.getMessage(); model.addAttribute("msg", msg); System.out.println(msg); } return "login"; }
首先,注意到代码中有一句: Subject subject = SecurityUtils.getSubject(); SecurityUtils是Shiro的一个工具类,可以获取到很多跟用户登录信息相关的东西。
当前获取了一个subject,这个subject可以获取当前用户的登录信息等,这里直接调用subject的login就行了。
注意login的参数,是一个UserPasswordToken,看到没,和我们前面在ShiroDBRealm中校验密码时用到的UserPasswordToken是同一个类。也就是说,后面校验密码时的对象,是从这里传进去的。
OK,到了这里,好像大功告成了。
纳尼,数据库中还没有用户数据?这可没法玩啊,也没法校验我们写的这么多代码是不是OK的。
if 你只是为了测验一下
可以直接在ShiroDBRealm的获取鉴权信息那个方法中将用户名、加密后的密码、salt写死返回就行了。
else
接下来就需要实现一个注册用户的controller,将用户数据写入数据库。
package org.rikey.web.controller; import org.apache.shiro.crypto.hash.Sha256Hash; import org.rikey.web.dao.UserDao; import org.rikey.web.domain.User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.annotation.Resource; import java.util.Random; @Controller public class Register { @Resource(name = "userDao") private UserDao userDao; private static final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int SALT_LENGTH = 6; @RequestMapping(value = "/register.htm", method = RequestMethod.GET) public String register(){ return "register"; } @RequestMapping(value = {"/doregister"}, method = RequestMethod.POST) public String doregister(User user, Model model) { try { String password = user.getPassword(); Random random = new Random(); random.setSeed(System.nanoTime()); StringBuffer sb = new StringBuffer(); for (int i = 0; i < SALT_LENGTH; i ++) { int number = random.nextInt(CHARS.length()); sb.append(CHARS.charAt(number)); } String salt = sb.toString(); String hashedPasswordBase64 = new Sha256Hash(password, salt).toBase64(); user.setPassword(hashedPasswordBase64); user.setSalt(salt); userDao.addUser(user); return "redirect:/login.htm"; } catch (Exception e) { model.addAttribute("errormsg", "添加用户失败"); return "error"; } } }
OK,注意,这里没有用到任何shiro框架结构,只用到了shiro的一个计算hash密码的方法。具体采用什么方法是跟前面的密码校验算法有关的,所以这里可以抽取出一个公共类,做成策略模式,在这里调用。
看前面的shiro spring配置,密码校验是采用的sha256算法,所以我们这里也采用Sha256Hash来进行加密。注意salt是随机生成的。OK,把这些数据写入数据库就行了。
至于数据库中User表的结构就不用多说了吧?id,用户名,密码,salt这些是必须的,其他的什么最近登录时间、错误重试次数等等,你能想到必须的都可以写进去。