Shiro会话管理和加密(快速上手!!!)

会话管理

Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Tomcat),不管是J2SE还是J2EE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO单点登录的支持等特性。

会话相关API

Subject.getSession():获取会话,等价于Subject.getSession(true),即如果当前没有创建session对象会创建一个;Subject.getSession(false),如果当前没有创建session对象则返回null。
session.setAttribute():设置会话属性
session.getAttribute():获取会话属性
session.removeAttribute(key):删除会话属性

SessionDAO

Shiro提供SessionDAO用于会话持久化,提供CRUD操作。

AbstractSessionDAO:提供了SessionDAO的基础实现,如生成会话ID等。
CachingSessioDAO:提供了对开发者透明的会话缓存的功能,需要设置相应的CacheManager。
MemorySessionDAO:直接在内存中进行会话维护。
EnterpriseCacheSessionDAO:提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
在实际开发中,如果要用到SessionDAO组件,可以自定义类实现自EnterpriseCacheSessionDAO类,为其注入sessionIdGenerator属性,如果用到缓存的话还可以注入一个缓存的实现,然后将这个SessionDAO组件注入给SessionManager(会话管理器),最后将SessionManager配置给SecurityManager。

会话使用

建议在开发中,Controller层使用原生的HttpSession对象,在Service层中使用Shiro提供的Session对象。如果在Service层中使用HttpSession对象,那么属于侵入式,并不建议这么做。
shiro提供的Session能够很好的解决这个问题。
在Controller中,通过request.getSession()获取会话session,该session到底来源于ServletRequest还是由Shiro管理并创建会话,主要由安全管理器SecurityManage和SessionManager会话管理器决定。
在使用默认SessionManager会话管理器的情况下,不管是通过request.getSession()或者subject.getSession()获取到session,操作session,两者都是等价的,请大家放心使用!

缓存

问题分析

每次在访问设置了权限的页面时,都会去执行doGetAuthorizationInfo()方法来获取权限信息判断当前用户是否具备访问权限。由于在实际情况中,权限是不会经常改变的,能否不用每次都去执行doGetAuthorizationInfo()方法获取权限呢?

解决方法

解决方法就是对权限授权数据进行缓存处理,我们会使用第三方的shiro-redis集成redis实现缓存。

具体实现

1.添加依赖

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.1.0</version>
</dependency>

2.application.properties配置文件中添加Redis配置

#Redis服务器地址
spring.redis.host=localhost
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
#spring.redis.password=foobared
#连接池最大连接数(使用负值表示没有权限)默认为8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认为-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接 默认8
spring.redis.lettuce.pool.max-idle=8
#连接池只能的最小空闲连接 默认0
spring.redis.lettuce.pool.min-idle=0
#Redis服务器超时时间
spring.redis.timeout=5000

3.改造ShiroConfig
ShiroConfig改造的步骤如下:

  • 注入Rrdis参数:@Value注解从application.properties配置文件中获取
  • 添加redisManager():创建RedisManager
  • 添加cacheManager():创建RedisCacheManager,注入RedisManager
  • 添加redisSessionDao():创建RedisSessionDAO,注入RedusSessionDAO
  • 修改myShiroRealm():创建MyShiroRealm,启用缓存
  • 修改securityManager():创建SecurityManager,注入MyShiroRealm、RedisCacheManager、SessionManager
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.LinkedCaseInsensitiveMap;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //注入Redis参数,从application.properties获得
    @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 RoleService roleService;

  /*  @Bean(name="shiroDialect")
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }*/


    /**
     * 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注释支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    public RedisManager redisManager(){
        RedisManager redisManager=new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
   /*     redisManager.setPassword(password);*/
        redisManager.setTimeout(timeout);
        return redisManager;
    }

    public RedisCacheManager cacheManager(){
        RedisCacheManager cacheManager=new RedisCacheManager();
        cacheManager.setRedisManager(redisManager());
        //缓存名称
        cacheManager.setPrincipalIdFieldName("usrName");
        //缓存有效时间
        cacheManager.setExpire(1800);
        return cacheManager;
    }

    //会话操作
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO sessionDAO=new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }

    //会话管理
    public DefaultWebSessionManager sessionManager(){
        DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return  sessionManager;
    }


    @Bean
    public MyShiroRealm myShiroRealm(){//自定义Realm
        MyShiroRealm shiroRealm=new MyShiroRealm();
        //设置启用缓存,并设置缓存名称
        shiroRealm.setCachingEnabled(true);
        shiroRealm.setAuthorizationCachingEnabled(true);
        shiroRealm.setAuthenticationCacheName("authorizationCache");
        //设置凭证(密码)匹配器
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    @Bean
    public SecurityManager securityManager(){//安全管理器SecurityManager
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();

        //注入Realm
        securityManager.setRealm(myShiroRealm());
        //注入缓存管理器
        securityManager.setCacheManager(cacheManager());
        //注入会话管理器
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){//Shiro过滤器:权限认证
        ShiroFilterFactoryBean shiroFilterFactory=new ShiroFilterFactoryBean();
        //注入SecurityManager
       shiroFilterFactory.setSecurityManager(securityManager);

       //权限验证:使用Filter控制资源(URL)的访问
        shiroFilterFactory.setLoginUrl("/dologin");
        shiroFilterFactory.setSuccessUrl("/main");
        shiroFilterFactory.setUnauthorizedUrl("/403");//没有权限跳转403页面
        Map<String,String> filterChianDefinitionMap=new LinkedCaseInsensitiveMap<String>();//必须使用LinkedHashMap(有序集合)
        //配置可以匿名访问的资源(URL):静态资源
        filterChianDefinitionMap.put("/css/**","anon");
        filterChianDefinitionMap.put("/fonts/**","anon");
        filterChianDefinitionMap.put("/images/**","anon");
        filterChianDefinitionMap.put("/js/**","anon");
        filterChianDefinitionMap.put("/localcss/**","anon");
        filterChianDefinitionMap.put("/localjs/**","anon");

        filterChianDefinitionMap.put("/login","anon");
        filterChianDefinitionMap.put("/logout","logout");//注销过滤器,自动注销

        //配置需要特定权限才能访问的资源(URL)

        //静态权限:包括全部需要特定权限才能访问的资源(URL)
       /* filterChianDefinitionMap.put("/user/list","perms[用户列表]");
        filterChianDefinitionMap.put("/user/add","perms[用户添加]");
        filterChianDefinitionMap.put("/user/edit","perms[用户修改]");
        filterChianDefinitionMap.put("/user/del","perms[用户删除]");*/

       //动态授权
        List<Right> rights=roleService.findAllRights();
        for(Right right:rights){
            if(right.getRightUrl()!=null&&!right.getRightUrl().trim().equals("")){
                filterChianDefinitionMap.put(right.getRightUrl(),"perms["+right.getRightCode()+"]");
            }
        }

        //配置认证访问,其他资源(URL)必须认证通过才能访问
        filterChianDefinitionMap.put("/**","authc");//必须放在过滤器链最后面

        shiroFilterFactory.setFilterChainDefinitionMap(filterChianDefinitionMap);
       return shiroFilterFactory;
    }
}

4.演示测试
登录成功后,访问“用户管理”功能,程序可能会报错,查看后台,错误如下:
错误信息
提示User不能转换为User,可是两个对象应该是相同的啊,继续查看错误出现位置:
在这里插入图片描述
发现是从Shiro中获取主身份凭证转换为User时报错,以前这里都是没有问题的,现在使用缓存后出错了,但是Shiro中获取到的主身份凭证的确是User对象。
经过仔细排查,发现错误的原因是我们的项目使用了热部署,造成类加载器不一致:项目启动时候加载项目当中的类所使用到的加载器是org.springframework boot.detls.restar.lassloader.RestartClassLoader,这是因为之前在项启动时候加载项目当中的类所使用到的加载器
是org.springframework.boot.devtools.restart.classloader.RestartClassoader, 这是因为之前在项目当中引入了spring boot devtools这个热部署包来提高效率。而从Shiro session取对象时所用到的类加载器并不是这个,而是sun.misc.Launcher.AppClassLoader,从而导致类型的转换异常。
问题找到了,解决办法有两个: 1、不使用热部署: 2、保证对引入的jar包使用相同的类加载器。此处我们肯定选择方案2,具体步骤如下:
1、在resources目录下新建META-INF目录
2、META-INF 目录下新建spring-devtools.properties文件
3、spring-devtools.properties 文件中添加以下两条配置信息,表示shiro-redis 和thymeleaf extras-shiro包下的类也使用RestartClassLoader类加载器:

restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
restart.include.thymeleaf-extras-shiro=/thymeleaf-extras-[\\w-\\.]+jar

在这里插入图片描述

加密

哈希与盐

如果你需要保存密码( 比如网站用户的密码),你要考虑如何保护这些密码数据,像那样直接将密码写入数据库中是极不安全的,因为任何可以打开数据库的人,都将可以直接看到这些密码,比如之前的600w CSDN账号泄露对用户可能造成很大损失。
解决的办法是将密码加密后再存储进数据库,比较常用的加密方法是使用哈希函数(散列算法),常见的散列算法如MD5 、SHA 等。哈希函数的具体定义,大家可以在网上或者相关书籍中查阅到,简单地说,它的特性如下:
1.原始密码经哈希 函计算后得到-一个哈希值
2.改变原始密码,哈希函数计算出的哈希值也会相应改变
3. 同样的密码,哈希值也是相同的
哈希函数是单向、不可逆的。也就是说从哈希值,你无法推算出原始的密码是多少。
一般进行散列时最好提供一个salt(盐), 加密领域的盐salt,不是炒菜的调料,而是为了提高加密的安全性。不管是对称加密,还是非对称加密,在用户信息和算法可能被泄漏的情况下,都存在密码被反推算出来的可能。在加密环节如果加盐,等于多了一重安全因素。一般来说,盐就是一个不被外界知道的随机字符串,把用户的明文密码加上盐,再进行加密得到密文(密码的加密后的形式)。
比如加密密码"admin",产生的散列值是"21232f297a57a5a743894a0e4a801fc3”,可以到一
些MD5解密网站很容易的通过散列值得到密码"admin",即如果直接对密码进行散列相对来
说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散
列的对象是“密码+用户名+ID", 这样生成的散列值相对来说更难破解。

@Test
public void testMd5Hash(){
    String password="123456";
    String salt="lili";
    Md5Hash md5Hash=new Md5Hash(password,salt);
    System.out.println(md5Hash.toString());
    //输出599b89b44301289555134992cacef601
}

加密与验证

Shiro提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务

public interface PasswordService {
    //输入明文密码得到密文密码
    String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;
}
public interface CredentialsMatcher {
    //匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密)
    boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);
}

具体实现

1.SimpleCredentialsMatcher
Shiro的所有开箱即用的Realm实现默认使用简单凭证匹配器SimpleCredentialsMatcher,这
意味着默认情况下密码的匹配只是简单的字符串比较。例如Realm的认证方法收到一个
UsernamePasswordToken,简单凭证匹配器会直接用Token中用户提交的密码和数据库的密码进行比较。当然,该匹配器除了支持String类型的比较,还支持char[]、 byte[]、 File 和InputStream等其它类型。
既然简单凭证匹配器SimpleCredentialsMatcher 只是简单的字符串比较,而数据库的密码应
该是经过加密之后的密码,那么UsernamePasswordToken中用户提交的密码也应该是经过加密之后的密码,但是用户在客户端是不可能提交经过加密的密码的,只会是提交明文密码(原始密码),所以我们需要在Controller中将客户端接收到的密码经过加密之后再封装到
UsernamePasswordToken中,然后再提交给SimpleCredentialsMatcher 进行匹配。
1、在UserService中添加加密方法:

@Override
public String encryptPassword(Object plaintextPassword) throws IllegalArgumentException {
    String salt="lili";
    Md5Hash md5Hash=new Md5Hash(plaintextPassword,salt,2);
    return md5Hash.toString();
}

此处使用Md5Hash进行加密,且固定使用”czkt”作为salt,加密2次。本来salt最好是用户
名+ ID作为salt,或是用户名+随机数作为salt,但这样就需要修改数据库,在sys_ _user 表中增加salt字段保存salt值,本次课就不那么麻烦了。另外,在整个系统中,User的添加和修改密码都需要使用userService.encryptPassword()方法对客户端接收的明文密码进行加密之后再存储到数据库中。
2、修改IndexController中的login方法,在封装UsernamePasswordToken时对密码进行加
密:

usrPassword=userService.encryptPassword(usrPassword);

登录次数限制

前面我们已经实现了密码加密和验证,大大提高了系统的安全性。在系统中,还可以对登
录失败次数进行限制。如在1个小时内密码最多重试5次,如果尝试次数超过5次就锁定1
小时,1小时后可再次重试,防止密码被暴力破解。
因为需要将锁定时间控制在一- 个规定时间内,此处可结合Redis 缓存实现,具体实现步骤
如下:
1、添加spring boot-starter data-redis依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、修改MyShiroRealm,注入StringRedisTemplate,在doGetAuthenticationInfo()方法中控制
登录次数,超过5次则锁定用户登录:

public class MyShiroRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //redis中的数据的key的前缀
    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 usrName=token.getUsername();
        //每次访问,登录次数+1
        ValueOperations<String,String> opsForValue=stringRedisTemplate.opsForValue();
        opsForValue.increment(SHIRO_LOGIN_COUNT+usrName,1);
        //计数大于5时,设置用户被锁定1分钟,清空登录技术
        if(Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT+usrName))>5){
            opsForValue.set(SHIRO_LOGIN_COUNT+usrName,"LOCK");
            stringRedisTemplate.expire(SHIRO_IS_LOCK+usrName,1, TimeUnit.MINUTES);
            stringRedisTemplate.delete(SHIRO_LOGIN_COUNT+usrName);//清空登录计数
        }
        if("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK+usrName))){
            throw new DisabledAccountException();
        }
        User user=userService.getUserByUsrName(usrName);
        if(user==null){
            throw  new UnknownAccountException();//账号错误
        }
        if(user.getUsrFlag()==null||user.getUsrFlag().intValue()==0){
            throw  new LockedAccountException();//账号锁定
        }
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(
                user,//身份(根据用户查询数据库获得用户)
                user.getUsrPassword(),//凭证(查询数据库获得的密码)
                ByteSource.Util.bytes("lili"),//加密使用的salt
                getName()//Realm对象的名称
        );
        //返回身份信息
        return info;
    }
}

3、修改IndexController,注入StringRedisTemplate,在login()登录方法中当登录成功时清空:

@Controller
public class LoginController {
    @Resource
    private UserService userService; 
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //redis中数据的key的前缀
    private String SHIRO_LOGIN_COUNT="shiro_login_count";

    @RequestMapping(value = "/dologin")
    public String login(String usrName, String usrPassword, Model model, HttpSession session){
try{
         /*  usrPassword=userService.encryptPassword(usrPassword);
           System.out.println("加密密码为:"+usrPassword);*/
           UsernamePasswordToken token=new UsernamePasswordToken(usrName,usrPassword);
           Subject subject= SecurityUtils.getSubject();
           subject.login(token);//认证、登录
           //认证(登录)成功,清空登录计数
           stringRedisTemplate.delete(SHIRO_LOGIN_COUNT+usrName);
           //认证(登录)成功
           User user=(User)subject.getPrincipal();

           //获取权限
           Role role=user.getRole();
           List<Right> rights=roleService.findRightsByRole(role);
           role.getRights().addAll(rights);
           session.setAttribute("loginUser",user);
           return "redirect:/main";
       }catch (UnknownAccountException| IncorrectCredentialsException e){
           model.addAttribute("error","用户名或密码错误,登录失败!");
           return "login";
       }catch (LockedAccountException e){
           model.addAttribute("error","用户被禁用,登陆失败!");
           return "login";
       }catch (AuthenticationException e){
           model.addAttribute("mag","认证异常,登录失败!");
           return "login";
       }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值