Shiro会话管理和加密
会话管理
Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Tomcat),不管是J2SE还是J2EE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO 单点登录的支持等特性。
接下来我们从几个方面了解一下Shiro的会话管理。
会话相关的API
- Subject.getSession():获取会话
- session.setAttribute(key,val):设置会话属性
- session.getAttribute(key):获取会话属性
- session.removeAttribute(key):删除会话属性
SessionDAO
shiro提供SessionDAO用于会话持久化,提供CRUD操作。
-
AbstractSessionDAO:提供了SesionDAO的基础实现,如生成会话ID等。
-
CachingSessionDAO:提供了对开发者透明的会话缓存的功能,需要设置相应的CacheManager.
-
MemorySessionDAO:直接在内存中进行会话维护。
-
EnterpriseCacheSessionDAO:提供了缓存功能的会话维护,默认情况下使用MapCache 实现,内部使用ConcurrentHashMap保存缓存的会话。
在实际开发中,如果要用到SessionDAO组件,可以自定义类实现自EnterpriseCacheSessionDAO类,为其注入sessionldGenerator属性,如果用到缓存的话还可以注入一个缓存的实现。然后将这个SesionDAO组件注入给SessionManager (会话管理器),最后将SessionManager配置给SecurityManager。下一小节我们将实现数据缓存,将使用到该组件。
缓存
具体实现
- 添加依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
- application.properties配置文件中添加Redis
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.password=8261
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
spring.redis.timeout=5000
- 改造ShiroConfig
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Resource
private IRoleService roleService;
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor attributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
public RedisCacheManager cacheManager(){
RedisCacheManager cacheManager=new RedisCacheManager();
cacheManager.setRedisManager(redisManager());
cacheManager.setPrincipalIdFieldName("userName");
cacheManager.setExpire(1800);
return cacheManager;
}
//会话操作
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO=new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
//会话管理
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm shiroRealm=new MyShiroRealm();
//启动缓存,设置缓存名称
shiroRealm.setCachingEnabled(true);
shiroRealm.setAuthorizationCachingEnabled(true);
shiroRealm.setAuthorizationCacheName("authorizationCache");
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(3);
return hashedCredentialsMatcher;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//注入Realm
securityManager.setRealm(myShiroRealm());
//注入缓存管理器
securityManager.setCacheManager(cacheManager());
//注入会话管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean//Shiro过滤
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactory=new ShiroFilterFactoryBean();
shiroFilterFactory.setSecurityManager(securityManager);
shiroFilterFactory.setLoginUrl("/user/login");
shiroFilterFactory.setSuccessUrl("/main");
shiroFilterFactory.setUnauthorizedUrl("/403");
Map<String,String> fileMap=new LinkedHashMap<String,String>();
fileMap.put("/css/**","anon");
fileMap.put("/fonts/**","anon");
fileMap.put("/images/**","anon");
fileMap.put("/js/**","anon");
fileMap.put("/localcss/**","anon");
fileMap.put("/localjs/**","anon");
fileMap.put("/user/dologin","anon");
fileMap.put("/user/logout","logout");
//静态授权
// fileMap.put("/user/list","perms[用户列表]");
// fileMap.put("/user/add","perms[用户添加]");
// fileMap.put("/user/edit/**","perms[用户编辑]");
// fileMap.put("/user/del/**","perms[用户删除]");
//动态授权
List<Right> rights=roleService.findAllRights();
for (Right right:rights){
if (right.getRightUrl()!=null && !right.getRightUrl().trim().equals("")){
fileMap.put(right.getRightUrl(),"perms["+right.getRightCode()+"]");
}
}
fileMap.put("/**","authc");
shiroFilterFactory.setFilterChainDefinitionMap(fileMap);
return shiroFilterFactory;
}
}
加密
哈希和盐
特点:
- 原始密码经过哈希函数计算后得到一个哈希值
- 改变原始密码,哈希函数计算出的哈希值也会相应改变
- 同样的密码,哈希值也是相同的
@Test
public void testMd5Hash(){
User user=new User("MyBatis-Plus","aaaaaa",null,null);
Md5Hash md5Hash=new Md5Hash(user.getUserPassword(),"",1);
String md5Salt=md5Hash.toString();
Md5Hash md5=new Md5Hash(user.getUserPassword(),md5Salt,3);
System.out.println(md5.toString());
}
加密和验证
Shiro提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务。
具体实现
SimpleCredentialsMatcher
- 在UserService中添加加密方法
@Override
public String enctyptPassword(Object plaintextPassword) throws IllegalArgumentException{
String salt=“czkt”;
Md5Hash md5Hash=new Md5Hash(plaintextPassword,salt,2);
return md5.toString();
}
- 修改IndexController中的login方法
@RequestMapping("/user/dologin")
public String dologin(String userName, String userPassword, Model model, HttpSession session){
try {
userPassword=userService.enctyptPassword(userPassword);
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(userName,userPassword);
subject.login(token);
//.....
}
}
HashedCredentialsMatcher
- 在配置类中创建HashedCredentialsMatcher并注入MyShiroRealm:
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(3);
return hashedCredentialsMatcher;
}
- 修改MyShiroRealm对象中的代码
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("调用MyShiroRealm.doGetAuthenticationInfo获取身份信息");
UsernamePasswordToken token=(UsernamePasswordToken)authenticationToken;
String userName=token.getUsername();
User user=userService.getUserByUserName(userName);
if (user==null){
throw new UnknownAccountException("账号错误");//账号错误
}
if (user.getUserFlag()==null || user.getUserFlag().intValue()==0){
throw new LockedAccountException("账号已锁定");//账号锁定
}
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user,
user.getUserPassword(),
ByteSource.Util.bytes(“czkt”),
getName());
return info;
}
登录次数限制
- 添加spring-boot-starter-data-redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 修改MyShiroRealm注入stringRedisTemplate,在doGetAuthenticationInfo控制登录次数
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
private String SHIRO_LOGIN_COUNT="shiro_login_count_";
private String SHIRO_IS_LOCK="shiro_is_lock_";
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("调用MyShiroRealm.doGetAuthenticationInfo获取身份信息");
UsernamePasswordToken token=(UsernamePasswordToken)authenticationToken;
String userName=token.getUsername();
ValueOperations<String,String> operations=stringRedisTemplate.opsForValue();
operations.increment(SHIRO_LOGIN_COUNT+userName,1);
if (Integer.parseInt(operations.get(SHIRO_LOGIN_COUNT+userName))>3){
operations.set(SHIRO_IS_LOCK+userName,"LOCK",1, TimeUnit.HOURS);
//stringRedisTemplate.expire(SHIRO_IS_LOCK+userName,1, TimeUnit.HOURS);
stringRedisTemplate.delete(SHIRO_LOGIN_COUNT+userName);
}
if ("LOCK".equals(operations.get(SHIRO_IS_LOCK+userName))){
throw new LockedAccountException();
}
User user=userService.getUserByUserName(userName);
if (user==null){
throw new UnknownAccountException("账号错误");//账号错误
}
if (user.getUserFlag()==null || user.getUserFlag().intValue()==0){
throw new LockedAccountException("账号已锁定");//账号锁定
}
Md5Hash md5Hash=new Md5Hash(new String(((UsernamePasswordToken)token).getPassword()),"",1);
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user,
user.getUserPassword(),
ByteSource.Util.bytes(md5Hash.toString()),
getName());
return info;
}
- 修改IndexController注入stringRedisTemplate,在登录方法中登录成功时清空
@RequestMapping("/user/dologin")
public String dologin(String userName, String userPassword, Model model, HttpSession session){
try {
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(userName,userPassword);
subject.login(token);
stringRedisTemplate.delete(SHIRO_LOGIN_COUNT+userName);
User user=(User) subject.getPrincipal();
//省略其他代码..
}
}
- 测试: