参考 shiro的使用
参考 初始shiro
流程:应用程序(Application)请求进行验证,将需要进行验证的账号密码存储在supject中,suoject将这些内容提交给SecurityManager。
【注】1)SecurityManager是shiro的核心,所有的认证,授权都要经过她的手。相当于 Spring MVC 中的 DispatcherServlet,是实现任务分配的核心。但是,注意,SecurityManager 仅仅实现任务的分配,它本身并不进行认证的工作,认证工作都是交给下 “下手” 去做的。
2)realm是一个数据源。在进行账号密码验证时,我们数据库中或者配置文件里必须存在正确的用户名和密码,realm需要做的就是获取这些用户名和密码,和shiro提供的用户名和密码进行比较,然后将结果返回给SecurityManager。
由此可以看出shiro 是由 SecurityManager 支配的,可以根据不同的请求将任务分配给不同的模块实现。
1)使用用户的登录信息创建令牌(token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
2)执行登录动作
SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject(); // 获取Subject单例对象
subject.login(token); // 登陆
【注】(1)SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。
(2)Subject中文翻译:项目。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。
3)判断用户
Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。
4)AuthorizationInfo,AuthenticationInfo(两个长得极其相似的英文单词)
在解释它们前首先必须要描述一下Shiro对于安全用户的界定:和大多数操作系统一样。用户具有角色和权限两种最基本的属性。例如,我的Windows登陆名称是learnhow,它的角色是administrator,而administrator具有所有系统权限。这样learnhow自然就拥有了所有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guest角色开放的权限极其有限。
同理,Shiro对用户的约束也采用了这样的方式。AuthenticationInfo代表了用户的角色信息集合,AuthorizationInfo代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上两个对象来判断。说到这里,大家可能还比较困惑。先不要着急,继续往后看就自然会明白了。
5)实现Realm
关键词:缓存机制、散列算法、加密算法
(1)缓存机制
Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map
【注】以上是ehcache.xml文件的基础配置,timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效。
(2)散列算法和加密算法
md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。
(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);
}
}
【注】因为散列算法无法复原,所以验证时,需要将用户输入的密码进行相同的算法,再与数据库中的对比
(4)匹配
CredentialsMatcher是一个接口,功能就是用来匹配用户登录使用的令牌和数据库中保存的用户信息是否匹配。当然它的功能不仅如此。本文要介绍的是这个接口的一个实现类: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一个提供AuthorizationInfo和AuthenticationInfo的地方。
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张表,分别是用户表(存储用户名,密码,盐等)、角色表(角色名称,相关描述等)、权限表(权限名称,相关描述等)、用户-角色对应中间表(以用户ID和角色ID作为联合主键)、角色-权限对应中间表(以角色ID和权限ID作为联合主键)。总之Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。
(6)会话
用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户停留在某个页面长时间无动作的时候,再次对任何链接的访问都会被重定向到登录页面要求重新输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是否包含User对象。启用Shiro会话管理的另一个用途是可以针对不同的模块采取不同的会话处理。以淘宝为例,用户注册淘宝以后可以选择记住用户名和密码。之后再次访问就无需登陆。但是如果你要访问支付宝或购物车等链接依然需要用户确认身份。当然,Shiro也可以创建使用容器提供的Session最为实现。
1)配置前端过滤器
Shiro_Project
index.jsp
SpringMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc.xml
1
true
SpringMVC
/
org.springframework.web.context.ContextLoaderListener
org.springframework.web.util.Log4jConfigListener
contextConfigLocation
classpath:spring.xml,classpath:spring-shiro-web.xml
log4jConfigLoaction
classpath:log4j.properties
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
true
targetFilterLifecycle
true
shiroFilter
/*
熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由于项目通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。
接着在classpath路径下配置spring-shiro-web.xml文件
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹配的条目之后容器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是: /authc/admin需要用户有用admin权限、/authc/**用户必须登录才能访问、/**其他所有路径任何人都可以访问。
2)代码
@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管理的会话对象,要获取依然必须通过Shiro。传统的Session中不存在User对象。
@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");
}
}