3.判断用户
Shiro 本身无法知道持有令牌的用户是否合法,因为除了项目设计者之外没有人知道。 因此java字符串加密,Realm是整个框架中为数不多的必须由设计者实现的模块之一。 当然,Shiro 提供了多种实现方式。 本文只介绍最常见也是最重要的实现方式——数据库查询。
4. 两个重要的英文单词
我在学习Shiro的过程中遇到的第一个障碍就是这两个对象的英文名称:AuthorizationInfo、AuthenticationInfo。 别怀疑你的眼睛,它们确实很像,不仅长得像,意思也很相似。
在解释它们之前,有必要描述一下Shiro对安全用户的定义:与大多数操作系统一样。 用户有两个基本属性,角色和权限。 比如我的Windows登录名是learnhow,它的角色是administrator,administrator拥有所有的系统权限。 这样,learnhow自然就拥有了所有的系统权限。 那么其他人需要登录我的电脑怎么办呢? 我可以开启一个guest角色,任何无法提供正确用户名和密码的未知用户都可以通过guest登录,而系统对guest角色的权限极其有限。
同理,Shiro 也用这种方法来限制用户。 AuthenticationInfo代表用户的角色信息集合,AuthorizationInfo代表角色的授权信息集合。 这样,当设计者在项目中对某个url路径设置了只允许某个角色或某个权限访问的控制约束时,Shiro就可以通过以上两个对象进行判断。 说到这里,大家可能还是一头雾水。 别着急,继续往回看,自然就明白了。
2.实现境界
如何实现Realm是本文的重头戏,也是比较麻烦的部分。 在这里你会接触到几个新鲜的概念:缓存机制、哈希算法、加密算法。 由于本文不会具体介绍这些概念,这里只是简单介绍几点,帮助大家更好的理解Shiro。
1.缓存机制
Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。 它的本质是将只能存在内存中的数据通过一种算法保存到硬盘中,然后根据需要依次取出。 你可以将Ehcache理解为一个Map对象,通过put保存对象,通过get获取对象。
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir" />
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
以上是ehcache.xml文件的基本配置。 timeToLiveSeconds 是缓存的最大生命周期,timeToIdleSeconds 是缓存的最大空闲时间。 当eternal为false时,ttl和tti才能生效。 更多配置的含义可以上网查看。
2.哈希算法和加密算法
md5是本文使用的hash算法,加密算法本文不做介绍。 散列和加密本质上都是将一个对象变成一串无意义的字符串。 不同的是散列后的对象无法恢复,是一个单向的过程。 例如,密码的加密通常采用哈希算法,如果用户忘记密码,只能修改密码,无法获取原始密码。 但是,信息的加密是一种形式化的加密算法,加密后的信息可以通过秘钥进行解密和恢复。
3. 用户注册
请注意,虽然我们一直在谈论用户登录安全性,但在谈到用户登录时,它是从用户注册开始的。 如何保证用户注册的信息不丢失、不泄露也是本项目设计的重点。
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
private String algorithmName = "md5";
private final int hashIterations = 2;
public void encryptPassword(User user) {
// User对象包含最基本的字段Username和Password
user.setSalt(randomNumberGenerator.nextBytes().toHex());
// 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
String newPassword = new SimpleHash(algorithmName, user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();
user.setPassword(newPassword);
}
}
如果不知道什么是加盐,可以忽略散列过程,只需要理解存储在数据库中的密码是根据用户注册时填写的密码生成的新字符串即可。 哈希密码替换用户注册时的密码,然后将用户保存到数据库中。 剩下的工作交给UserService去处理。
所以这带来了一个新的问题。 既然hash算法无法恢复,那么我们应该如何判断用户在登录时使用了密码呢? 答案是用户密码需要用相同的算法再次进行哈希处理,然后与数据库中存储的字符串进行比较。
4.匹配
CredentialsMatcher是一个接口,其作用是匹配用户登录使用的token是否与数据库中保存的用户信息相匹配。 当然它的作用不止于此。 本文将介绍该接口的一个实现类:HashedCredentialsMatcher
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入
private Cache passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 自定义一个验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间
if (retryCount.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}
可以看出,在这个实现中,设计者只是加了一个不允许连续错误登录的判断。 真正的匹配过程还是交给它的直接父类来完成。 连续登录错误的判断是通过Ehcache缓存实现的。 显然 match 对于成功的匹配返回 true。
5.获取用户角色和权限信息
说了这么多,就到了我们的重点,Realm。 如果了解了Shiro的用户匹配和注册加密的整个过程,真正理解Realm的实现就相对简单了。 我们还得回到上面提到的两个非常相似的对象 AuthorizationInfo 和 AuthenticationInfo。 因为Realm就是提供这两个对象的地方。
public class UserRealm extends AuthorizingRealm {
// 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
private UserService userService = new UserServiceImpl();
/**
* 提供用户信息返回权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 根据用户名查询当前用户拥有的角色
Set roles = userService.findRoles(username);
Set roleNames = new HashSet();
for (Role role : roles) {
roleNames.add(role.getRole());
}
// 将角色名称提供给info
authorizationInfo.setRoles(roleNames);
// 根据用户名查询当前用户权限
Set permissions = userService.findPermissions(username);
Set permissionNames = new HashSet();
for (Permission permission : permissions) {
permissionNames.add(permission.getPermission());
}
// 将权限名称提供给info
authorizationInfo.setStringPermissions(permissionNames);
return authorizationInfo;
}
/**
* 提供账户信息返回认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if (user == null) {
// 用户名不存在抛出异常
throw new UnknownAccountException();
}
if (user.getLocked() == 0) {
// 用户被管理员锁定抛出异常
throw new LockedAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),
user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());
return authenticationInfo;
}
}
按照Shiro的设计思路,用户和角色的关系是多对多的,角色和权限的关系也是多对多的。 因此,需要在数据库中创建5张表,分别是:
具体dao和service的实现本文不做介绍。 总之,结论是Shiro需要先根据用户名和密码判断登录用户是否合法,然后对合法用户进行授权。 而这个过程就是Realm的实现过程。
6.对话
一个用户的登录就是一个session,Shiro也可以代替Tomcat等容器来管理session。 目的是当用户长时间停留在某个页面没有任何操作时,访问任何一个链接都会被重定向到登录页面要求重新输入用户名和密码,而不需要程序员不断判断是否会话在 Servlet 中。 包含用户对象。
启用 Shiro 会话管理的另一个用途是对不同的模块采取不同的会话处理。 以淘宝为例java字符串加密,用户在淘宝注册后可以选择记住用户名和密码。 之后,您无需再次登录。 但是,如果要访问支付宝或购物车等链接,用户仍然需要确认身份。 当然Shiro也可以创建容器提供的Session来实现最。
3、与SpringMVC的集成
有了注册模块和Realm模块的支持,下面就是如何与SpringMVC进行集成开发。 有过框架集成经验的同学一定知道,所谓的集成基本上就是一堆xml文件的配置,Shiro也不例外。
1.配置前端过滤器
先说个题外话,Filter是过滤器,interceptor是拦截器。 前者基于回调函数实现,必须依赖容器支持。 因为容器需要把整个FilterChain组装起来,一个一个调用。 后者是基于proxy实现的,属于AOP的范畴。
如果要在WEB环境下使用Shiro,首先要在web.xml文件中进行配置
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Shiro_Project</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
</context-param>
<context-param>
<param-name>log4jConfigLoaction</param-name>
<param-value>classpath:log4j.properties</param-value>
</context-param>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<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>
</web-app>
熟悉Spring配置的同学可以重点关注绿色注释部分,这是让Shiro生效的关键。 由于项目由Spring管理,所以原则上所有的配置都交给Spring。 DelegatingFilterProxy的作用是通知Spring把所有的Filter都交给ShiroFilter管理。
然后在classpath路径下配置spring-shiro-web.xml文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
<bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager" />
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="2" />
<property name="storedCredentialsHexEncoded" value="true" />
</bean>
<bean id="userRealm" class="utils.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/" />
<property name="unauthorizedUrl" value="/" />
<property name="filterChainDefinitions">
<value>
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
</value>
</property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
需要注意的是,filterChainDefinitions过滤器中path的配置是有顺序的,当找到匹配的entry时容器不会继续搜索。 因此带有通配符的路径紧随其后。 三个配置的含义是:
说了这么多,大家一定很关心Spring引入Shiro后怎么写登录代码。
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping("login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕获密码错误异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "password error!");
return mv;
} catch (UnknownAccountException uae) {
// 捕获未知用户名异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "username error!");
return mv;
} catch (ExcessiveAttemptsException eae) {
// 捕获错误登录过多的异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "times error");
return mv;
}
User user = userService.findByUsername(username);
subject.getSession().setAttribute("user", user);
return new ModelAndView("success");
}
}
登录完成后,将当前用户信息保存到Session中。 这个Session是Shiro管理的session对象,还是要通过Shiro获取。 User对象在传统的Session中是不存在的。
@Controller
@RequestMapping("authc")
public class AuthcController {
// /authc/** = authc 任何通过表单登录的用户都可以访问
@RequestMapping("anyuser")
public ModelAndView anyuser() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
// /authc/admin = user[admin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面
@RequestMapping("admin")
public ModelAndView admin() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
}