Shiro 会话管理和加密

Shiro 会话管理和加密

会话管理

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

会话相关API

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

SessionDAO
Shiro 提供 SessionDao 用于会话持久化。提供 CRUD 操作。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200922083219300.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTg1NzkyNg==,size_16,color_FFFFFF,t_70#pic_center

  • AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等。
  • CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager。
  • MemorySessionDAO 直接在内存中进行会话维护。
  • EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。

在实际开发中,如果要用到 SessionDAO 组件,可以自定义类实现自 EnterpriseCacheSessionDAO 类,为其注入 sessionIdGenerator 属性,如果用到缓存的话还可以注入一个缓存的名字。最后将这个 SesionDAO 组件注入给 SessionManager(会话管理器),最后将 SessionManager 配置给 SecurityManager。

会话使用
建议在开发中,Controller 层使用原生的 HttpSession 对象,在 Service 层中使用 Shiro 提供的 Session 对象。如果在 Service 层中使用 HttpSession 对象,那么属于侵入式,并不建议这么做。Shiro 提供的 Session 能够很好的解决这个问题。

那么,问题来了,两种方式获取的 session 是否相同呢?

在 Controller 中,通过 request.getSession() 获取会话 session ,该 session 到底来源于
ServletRequest 还是由 Shiro 管理并创建的会话,主要由安全管理器 SecurityManager 和
SessionManager 会话管理器决定。

在使用默认 SessionManager 会话管理器的情况下,不管是通过 request.getSession() 或者 subject.getSession() 获取到 session,操作 session,两者都是等价的,请大家放心使用!
在这里插入图片描述
在这里插入图片描述

缓存

使用第三方的 shiro-redis 集成 redis 实现缓存

  1. 添加依赖
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.1.0</version>
</dependency>
  1. application.properties 配置文件中添加 Redis 配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=hxy
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阳塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认日
spring.redis.lettuce.pool.min-idle=0
#Redis服务超时时间
spring.redis.timeout=5000
  1. ShiroConfig.java
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.crm.pojo.Right;
import com.crm.service.RoleService;
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 javax.annotation.Resource;
import java.util.LinkedHashMap;
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
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //使用 md5 算法进行加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列次数, 意为加密几次
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {//thymeleaf页面上使用shiro标签
        return new ShiroDialect();
    }

    /**
     * 开启shiro注解(如@RequiresRoles,@RequiresPermissions),
     * 需借助SpringAOP扫描使用shiro注解类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
     */
    @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("userName");
        //缓存有效时间
        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.setAuthorizationCacheName("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 shiroFilterFactory(SecurityManager securityManager) {//Shiro过滤器:权限验证
        ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
        //注入SecurityManager
        shiroFilterFactory.setSecurityManager(securityManager);
        //权限验证:使用Filter控制资源(URL)的访问
        shiroFilterFactory.setLoginUrl("/login");
        shiroFilterFactory.setSuccessUrl("/main");
        shiroFilterFactory.setUnauthorizedUrl("/403");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();//必须使用LinkedHashMap(有序集合)
        //配置可以匿名访问的资源(URL): 静态资源
        filterChainDefinitionMap.put("/statics/css/**", "anon");
        filterChainDefinitionMap.put("/statics/fonts/**", "anon");
        filterChainDefinitionMap.put("/statics/images/**", "anon");
        filterChainDefinitionMap.put("/statics/js/**", "anon");
        filterChainDefinitionMap.put("/statics/localcss/**", "anon");
        filterChainDefinitionMap.put("/statics/lcaljs/**", "anon");

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

        //配置需要特定权限才能访问的资源(URL)
        //静态授权:包括全部需要特定权限才能访问的资源(URL)
        /*filterChainDefinitionMap.put("/list","perms[用户列表]");
        filterChainDefinitionMap.put("/add","perms[用户添加]");
        filterChainDefinitionMap.put("/modify","perms[用户编辑]");
        filterChainDefinitionMap.put("/del","perms[用户删除]");*/

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

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

        shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactory;
    }

}

在测试中如出现了 java.lang.ClassCastException: com.crm.pojo.User cannot be cast to com.crm.pojo.User 异常, 原因是我们的项目可能使用了热部署, 所以造成类加载器不一致所导致。
解决方案:
在 resource 目录下新建 META-INF 目录,在 META-INF 目录下新建 spring-devtools.properties文件,在 spring-devtools.properties 文件中添加以下两条配置信息:

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

配置完成后重启服务就OK了。

加密

哈希与盐
如果你需要保存密码(比如网站用户的密码),你要考虑如何保护这些密码数据,像下面那样直接将密码写入数据库中是极不安全的,因为任何可以打开数据库的人,都将可以直接看到这些密码,比如之前的600w 优快云账号泄露对用户可能造成很大损失。
在这里插入图片描述
解决的办法是将密码加密后再存储进数据库,比较常用的加密方法是使用哈希函数(散列算法),常见的散列算法如 MD5、SHA 等。哈希函数的具体定义,大家可以在网上或者相关书籍中查阅到,简单地说,它的特性如下:

  • 原始密码经哈希函数计算后得到一个哈希值
  • 改变原始密码,哈希函数计算出的哈希值也会相应改变
  • 同样的密码,哈希值也是相同的

哈希函数是单向、不可逆的。也就是说从哈希值,你无法推算出原始的密码是多少。

一般进行散列时最好提供一个 salt(盐),加密领域的盐 salt,不是炒菜的调料,而是为了提高加密的安全性。不管是对称加密,还是非对称加密,在用户信息和算法可能被泄漏的情况下,都存在密码被反推算出来的可能。在加密环节如果加盐,等于多了一重安全因素。一般来说,盐就是一个不被外界知道的随机字符串,把用户的明文密码加上盐,再进行加密得到密文(密码的加密后的形式)。

比如加密密码 “admin” ,产生的散列值是 “21232f297a57a5a743894aOe4a801fc3”,可以到一些 MD5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是"密码+用户名+ID",这样生成的散列值相对来说更难破解。

@Test
    public void testMd5Hash(){
        String password = "admin";
        String salt = "zxcv";
        Md5Hash  md5Hash = new Md5Hash(password,salt,2);
        System.out.println(md5Hash.toString());
        //输出: b8421ab1ac24e33a5cea864549c054e6
    }

如上代码通过盐 “zxcv” MD5 散列 “admin”。2 表示散列次数,

加密与验证+登录次数限制
加依赖

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

如上的 ShiroConfig :

@Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //使用 md5 算法进行加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列次数, 意为加密几次
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }
@Bean
    public MyShiroRealm myShiroRealm() {//自定义Realm
        MyShiroRealm shiroRealm = new MyShiroRealm();
        //设置启用缓存,并设置缓存名称
        shiroRealm.setCachingEnabled(true);
        shiroRealm.setAuthorizationCachingEnabled(true);
        shiroRealm.setAuthorizationCacheName("authorizationCache");
        //设置凭证匹配器
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

加 MyShiroRealm.java

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;

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 AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("调用MyShiroRealm.doGetAuthorizationInfo获取权限信息!");
        //获得权限信息
        User user = (User) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //静态授权:授权主体(用户)相应的角色和权限
        /*info.addRole(user.getRole().getRoleName());
        info.addStringPermission("用户列表");//所有用户拥有用户列表权限
        if("管理员".equals(user.getRole().getRoleName())){//管理员拥有"增删改"权限
            info.addStringPermission("用户添加");
            info.addStringPermission("用户编辑");
            info.addStringPermission("用户删除");
        }*/

        //动态授权
        Role role = user.getRole();
        if(role!=null){
            info.addRole(user.getRole().getRoleName());
            Set<Right> rights = role.getRights();
            if(rights!=null && rights.size()>0){
                for (Right right :rights){
                    info.addStringPermission(right.getRightCode());
        }
    }
        }
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("调用MyShiroRealm.doGetAuthenticationInfo获取身份信息!");
        //获得身份信息
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String userName = token.getUsername();
        //每次访问,登录次数+1
        ValueOperations<String,String> opsForValue = stringRedisTemplate.opsForValue();
        opsForValue.increment(SHIRO_LOGIN_COUNT+userName,1);
        //计数大于5时,设置用户被锁定一小时,清空登录计数
        if (Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT + userName)) > 2) {
            opsForValue.set(SHIRO_IS_LOCK + userName, "LOCK",1, TimeUnit.MINUTES);
            stringRedisTemplate.delete(SHIRO_LOGIN_COUNT + userName);
        }
        if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK + userName))) {
            throw new DisabledAccountException();
        }
        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("zxcv"),//加密使用的salt
                getName()//Realm对象的名称
        );
        //返回身份信息
        return info;

    }

}

控制器:

@Controller
public class LoginController {

    @Resource
    private UserService userService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private String SHIRO_LOGIN_COUNT = "shiro_login_count_";

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(String userName, String userPassword, Model model, HttpSession session) {
        try {
            UsernamePasswordToken token = new UsernamePasswordToken(userName,userPassword);
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);//认证,登录
            stringRedisTemplate.delete(SHIRO_LOGIN_COUNT+userName);
            //认证(登录)成功
            User user = (User)subject.getPrincipal();
            session.setAttribute("loginUser",user);
            System.out.println(user);
            return "main";
        } catch (UnknownAccountException| IncorrectCredentialsException e) {
            model.addAttribute("userName",userName);
            model.addAttribute("userPassword",userPassword);
            model.addAttribute("error","用户名或密码错误,登录失败!");
            return "login";
        } catch (LockedAccountException e) {
            model.addAttribute("userName",userName);
            model.addAttribute("userPassword",userPassword);
            model.addAttribute("error","用户被禁用,登录失败!");
            return "login";
        } catch (AuthenticationException e) {
            model.addAttribute("userName",userName);
            model.addAttribute("userPassword",userPassword);
            model.addAttribute("error","多次错误,账号已被锁定!");
            return "login";
        }
    }
}

测试时记得启动 Redis

完成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值