• Apache Shiro 会话+缓存+记住我(三)


    1、会话管理SessionDao和SessionManager

    1)安装Redis
    2)依赖

            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>2.8.0</version>
            </dependency>
    

    3)配置redis连接池的bean:

        @Bean
        public JedisPoolConfig getJedisPoolConfig() {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            return jedisPoolConfig;
        }
    
        @Bean
        public JedisPool getJedisPool() {
            JedisPool jedisPool = new JedisPool(getJedisPoolConfig(), "129.204.58.30", 6379);
            return jedisPool;
        }
    

    4)编写redis工具类:

    package com.example.demo_mg.util;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    
    import java.util.Set;
    
    @Component
    public class RedisUtil {
        @Autowired
        private JedisPool jedisPool;
    
        private Jedis getResource() {
            return jedisPool.getResource();
        }
    
        public byte[] set(byte[] key, byte[] value) {
            Jedis jedis = getResource();
            try {
                jedis.set(key, value);
                return value;
            }  finally {
                jedis.close();
            }
        }
    
        public void expire(byte[] key, int i) {
            Jedis jedis = getResource();
            try {
                jedis.expire(key, i);
            }  finally {
                jedis.close();
            }
        }
    
        public byte[] get(byte[] key) {
            Jedis jedis = getResource();
            try {
                return jedis.get(key);
            }  finally {
                jedis.close();
            }
        }
    
        public void del(byte[] key) {
            Jedis jedis = getResource();
            try {
                jedis.del(key);
            }  finally {
                jedis.close();
            }
        }
    
        //获取指定前缀所有Key
        public Set<byte[]> keys(String prefix) {
            Jedis jedis = getResource();
            try {
                return jedis.keys((prefix + "*").getBytes());
            }  finally {
                jedis.close();
            }
        }
    }
    

    5)编写SessionDao继承AbstractSessionDAO:

    package com.example.demo_mg.session;
    
    import com.example.demo_mg.util.RedisUtil;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.SerializationUtils;
    
    import javax.annotation.Resource;
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    
    @Component
    public class RedisSessionDao extends AbstractSessionDAO {
        @Resource
        private RedisUtil redisUtil;
    
        private final String SHIRO_SESSION_PREFIX = "shiro_session:";
    
        private byte[] getKey(String key) {
            return (SHIRO_SESSION_PREFIX + key).getBytes();
        }
    
        private void saveSession(Session session) {
            if(session != null && session.getId() !=null) {
                byte[] key = getKey(session.getId().toString());
                byte[] value = SerializationUtils.serialize(session);
                redisUtil.set(key, value);
                redisUtil.expire(key, 600);
            }
        }
    
        @Override
        protected Serializable doCreate(Session session) {
            Serializable sessionId = generateSessionId(session);
            assignSessionId(session, sessionId); //需要捆绑,否则登录会抛异常
            saveSession(session);
            return sessionId;
        }
    
    //    serializable是sessionId
        @Override
        protected Session doReadSession(Serializable serializable) {
            System.out.println("read session");
            if(serializable == null) {
                return null;
            }
            byte[] key = getKey(serializable.toString());
            byte[] value = redisUtil.get(key);
            return (Session) SerializationUtils.deserialize(value);
        }
    
        @Override
        public void update(Session session) throws UnknownSessionException {
            saveSession(session);
        }
    
        @Override
        public void delete(Session session) {
            if(session == null || session.getId() == null) {
                return;
            }
            byte[] key = getKey(session.getId().toString());
            redisUtil.del(key);
        }
    
        @Override
        public Collection<Session> getActiveSessions() {
            Set<byte[]> keys = redisUtil.keys(SHIRO_SESSION_PREFIX);
            Set<Session> sessions = new HashSet<>();
            if(CollectionUtils.isEmpty(keys)) {
                return sessions;
            }
            for (byte[] key : keys) {
                Session session = (Session) SerializationUtils.deserialize(redisUtil.get(key));
                sessions.add(session);
            }
            return sessions;
        }
    }
    

    6)在(二)的基础上修改配置bean:

    新配置两个bean
        @Bean
        public RedisSessionDao getRedisSessionDao() {
            RedisSessionDao redisSessionDao = new RedisSessionDao();
            return redisSessionDao;
        }
    
        @Bean
        public DefaultWebSessionManager getDefaultWebSessionManager(RedisSessionDao redisSessionDao) {
            DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
            defaultWebSessionManager.setSessionDAO(redisSessionDao);
            return defaultWebSessionManager;
        }
    
    修改一个bean(添加会话管理)
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager(TestRealm testRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(testRealm);
    
            //会话管理
            securityManager.setSessionManager(getDefaultWebSessionManager(getRedisSessionDao()));
    
            return  securityManager;
        }
    

    7)登录验证,只是登录,后台打印了5次read session,redis客户端执行keys *,查到"shiro_session:19b704b8-3324-4f88-92c0-c0effb5243a6"。
    源码在DefaultSessionManager的retrieveSession方法中,调用Session s = this.retrieveSessionFromDataSource(sessionId);该方法中又调用this.sessionDAO.readSession(sessionId);再调AbstractSessionDao的readSession方法,代码Session s = this.doReadSession(sessionId);调用自己实现的RedisSessionDao的doReadSession方法。所有,要减少去Redis读取session的次数,需要自己重写retrieveSession方法,改造Session s = this.retrieveSessionFromDataSource(sessionId);。

    定义一个SessionManager类继承DefaultWebSessionManager类重写retrieveSession方法:

    package com.example.demo_mg.session;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.SessionKey;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.apache.shiro.web.session.mgt.WebSessionKey;
    
    import javax.servlet.ServletRequest;
    import java.io.Serializable;
    
    /**
     * sessionKey对象里有request对象,可以第一次查询以后把session放在request对象里,就不需要频繁查询redis。
     */
    public class RedisSessionManager extends DefaultWebSessionManager {
        @Override
        protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
            Serializable sessionId = getSessionId(sessionKey);
            ServletRequest request = null;
            if(sessionKey instanceof WebSessionKey) {
                request = ((WebSessionKey)sessionKey).getServletRequest();
            }
            if(request != null && sessionId != null) {
                Session session = (Session) request.getAttribute(sessionId.toString());
                if(session != null) {
                    return session;
                }
            }
            Session session = super.retrieveSession(sessionKey);
            if(request != null && sessionId != null) {
                request.setAttribute(sessionId.toString(), session);
            }
            return session;
        }
    }
    

    修改配置bean:

        @Bean
        public DefaultWebSessionManager getDefaultWebSessionManager(RedisSessionDao redisSessionDao) {
    //        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
            DefaultWebSessionManager defaultWebSessionManager = new RedisSessionManager(); //改用自己定义的SessionManager
            defaultWebSessionManager.setSessionDAO(redisSessionDao);
            return defaultWebSessionManager;
        }
    

    2、缓存管理,Cache和CacheManager:

    缓存角色、权限信息,授权需要,认证可以不用,可以用redis、echache或map实现缓存CacheManager

    1)自定义Cache:

    package com.example.demo_mg.cache;
    
    import com.example.demo_mg.util.RedisUtil;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.springframework.stereotype.Component;
    import org.springframework.util.SerializationUtils;
    
    import javax.annotation.Resource;
    import java.util.Collection;
    import java.util.Set;
    
    @Component
    public class RedisCache<K, V> implements Cache<K, V> {
        @Resource
        private RedisUtil redisUtil;
    
        private final String CACHE_PREFIX = "shiro_cache:";
    
        private byte[] getKey(K k) {
            if(k instanceof String) {
                return (CACHE_PREFIX + k).getBytes();
            }
            return SerializationUtils.serialize(k);
        }
    
        //该方法还可以用Map在本地做二级缓存
        @Override
        public V get(K k) throws CacheException {
            System.out.println("缓存读取授权信息");
            byte[] value = redisUtil.get(getKey(k));
            if(value != null) {
                return (V)SerializationUtils.deserialize(value);
            }
            return null;
        }
    
        //过期时间最好可配置,单位秒
        @Override
        public V put(K k, V v) throws CacheException {
            byte[] key = getKey(k);
            byte[] value = SerializationUtils.serialize(v);
            redisUtil.set(key, value);
            redisUtil.expire(key, 600);
            return v;
        }
    
        @Override
        public V remove(K k) throws CacheException {
            byte[] key = getKey(k);
            byte[] value = redisUtil.get(key);
            redisUtil.del(key);
            if(value != null) {
                return (V)SerializationUtils.deserialize(value);
            }
            return null;
        }
    
        @Override
        public void clear() throws CacheException {
            //只清空缓存用的,慎重,小心将Redis的所有缓存清空
        }
    
        @Override
        public int size() {
            return 0;
        }
    
        @Override
        public Set<K> keys() {
            return null;
        }
    
        @Override
        public Collection<V> values() {
            return null;
        }
    }
    

    2)自定义CacheManager:

    package com.example.demo_mg.cache;
    
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.apache.shiro.cache.CacheManager;
    
    import javax.annotation.Resource;
    
    
    public class RedisCacheManager implements CacheManager {
        @Resource
        private RedisCache redisCache;
    
        //这里的String s可以用来创建一个concurrentHashMap缓存cache名称和cache实例,s就是cache名称,这里只有一个redisCache对象就不用map了。
        @Override
        public <K, V> Cache<K, V> getCache(String s) throws CacheException {
            return redisCache;
        }
    }
    

    3)配置类:

    新增bean
        @Bean
        public RedisCacheManager getRedisCacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            return redisCacheManager;
        }
    
    修改bean(添加缓存管理)
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager(TestRealm testRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(testRealm);
    
            //会话管理
            securityManager.setSessionManager(getDefaultWebSessionManager(getRedisSessionDao()));
    
            //缓存管理
            securityManager.setCacheManager(getRedisCacheManager());
    
            return  securityManager;
        }
    

    4)在Realm添加标记(只加上两处System.out打印开始和结束从数据库获取授权信息):

    package com.example.demo_mg.realm;
    
    import org.apache.commons.collections.map.HashedMap;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    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 java.util.*;
    
    public class TestRealm extends AuthorizingRealm {
        //模拟users、user_roles、roles_permissions三张表的查询,实际应用需要查询数据库或缓存
        Map<String, String> users = new HashMap<>();
        Map<String, Set<String>> user_roles = new HashedMap();
        Map<String, Set<String>> roles_permissions = new HashedMap();
    //    String salt = UUID.randomUUID().toString().replaceAll("-","");
    
        {
            //不加盐(与认证对应)
            users.put("wzs", new Md5Hash("123456",null, 2).toString());
            //加盐
    //        users.put("wzs", new Md5Hash("123456",salt, 2).toString());
            user_roles.put("wzs", new HashSet<>(Arrays.asList("admin", "test")));
            roles_permissions.put("admin", new HashSet<>(Arrays.asList("user:delete", "user:update")));
            super.setName("TestRealm"); //设置Realm名称,可选
        }
    
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //从认证信息获取用户名
            String username = (String)principalCollection.getPrimaryPrincipal();
            //从数据库或缓存中获取角色、权限数据
            System.out.println("数据库获取认证信息start:");
            Set<String> roles = user_roles.get(username);
            Set<String> permissions = new HashSet<>();
            for (String role : roles) {
                Set<String> set;
                if((set = roles_permissions.get(role)) != null) {
                    permissions.addAll(set);
                }
            }
            System.out.println("数据库获取认证信息end.");
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.setRoles(roles);
            simpleAuthorizationInfo.setStringPermissions(permissions);
            return simpleAuthorizationInfo;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //从主题传过来的认证信息中,获得用户名
            String username = (String)authenticationToken.getPrincipal();
            //通过用户名从数据库中获取凭证
            String password = users.get(username);
            if(password != null) {
                //不加盐
    //            return new SimpleAuthenticationInfo(username, password, super.getName());
                //加盐
                SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, super.getName());
    //            simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
                return simpleAuthenticationInfo;
            }
            return null;
        }
    }
    

    5)测试结果:
    第一次访问带有@RequireRole注解的方法后台打印:
    read session
    缓存读取授权信息
    数据库获取认证信息start:
    数据库获取认证信息end.

    第二次访问后台打印:
    read session
    缓存读取授权信息

    说明第一次从缓存没取到,去数据库获取并缓存起来,以后直接从缓存获取。

    Redis客户端执行keys *,有如下key:

     "xacxedx00x05srx002org.apache.shiro.subject.SimplePrincipalCollectionxa                                     8x7fX%xc6xa3Jx03x00x01Lx00x0frealmPrincipalstx00x0fLjava/util/Map;xp                                     srx00x17java.util.LinkedHashMap4xc0N\x10lxc0xfbx02x00x01Zx00x0bacces                                     sOrderxrx00x11java.util.HashMapx05axdaxc1xc3x16`xd1x03x00x02Fx00
    l                                     oadFactorIx00	thresholdxp?@x00x00x00x00x00x0cwx00x00x00x10x00x00                                     x00x01tx00	TestRealmsrx00x17java.util.LinkedHashSetxd8lxd7Zx95xdd*x1e                                     x02x00x00xrx00x11java.util.HashSetxbaDx85x95x96xb8xb74x03x00x00xpw                                     x0cx00x00x00x02?@x00x00x00x00x00x01tx00x03wzsxxx00wx01x01qx00~                                     x00x05x"
    

    3、RememberMe,记住我,实现自动登录,在User对象中添加boolean类型的remeberMe属性,登录表单添加"记住我"checkbox:
    1)改造登录方法

        @RequestMapping(value = "/login",method = RequestMethod.GET)
        public String loginUser(String username, String password, boolean rememberMe) {
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
            Subject subject = SecurityUtils.getSubject();
            try {
                usernamePasswordToken.setRememberMe(rememberMe);  //记住我功能
                subject.login(usernamePasswordToken);   //完成登录
                //更新用户登录时间,也可以在ShiroRealm里面做
                return "index";
            } catch(Exception e) {
                return "login";//返回登录页面
            }
        }
    

    2)配置bean

    添加2个bean
        @Bean
        public SimpleCookie getSimpleCookie() {
            SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); //生成cookie名称
            simpleCookie.setMaxAge(600); //生成cookie过期时间,单位秒
            return simpleCookie;
        }
    
        @Bean
        public CookieRememberMeManager getCookieRememberMeManager() {
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            cookieRememberMeManager.setCookie(getSimpleCookie());
            return cookieRememberMeManager;
        }
    
    修改一个bean(添加RememberMe)
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager(TestRealm testRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(testRealm);
    
            //会话管理
            securityManager.setSessionManager(getDefaultWebSessionManager(getRedisSessionDao()));
    
            //缓存管理
            securityManager.setCacheManager(getRedisCacheManager());
    
            //RememberMe
            securityManager.setRememberMeManager(getCookieRememberMeManager());
    
            return  securityManager;
        }
    

    3)验证:登录以后浏览器F12的Application下,Cookies下,http://localhost:8080有一条叫做rememberMe的cookie。且后台重启以后页面没调转到登录页,说明记住我生效了。

  • 相关阅读:
    黄聪:电子商务关键数字优化(线上部分,上)
    黄聪:Wordpress如何在主题模板中调用菜单?
    黄聪:WordPress for SAE在Windows下使用SVN部署代码
    黄聪:Ubuntu下使用低版g++编译器编译TSE
    黄聪:如何使用WordPress 2.9内置文章缩略图功能(Post Thumbnail)
    黄聪:相关词句采集与分析研究
    黄聪:JQuery鼠标放上后链接平滑移动效果WordPress插件
    黄聪:TSE分析及完全注释[6] 倒排索引的建立的程序分析(转)
    黄聪:buffer overflow detected问题解决及gcc4.1安装
    黄聪:VMware安装Ubuntu10.10【图解】转
  • 原文地址:https://www.cnblogs.com/kibana/p/11111529.html
Copyright © 2020-2023  润新知