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:

 "\xac\xed\x00\x05sr\x002org.apache.shiro.subject.SimplePrincipalCollection\xa                                     8\x7fX%\xc6\xa3\bJ\x03\x00\x01L\x00\x0frealmPrincipalst\x00\x0fLjava/util/Map;xp                                     sr\x00\x17java.util.LinkedHashMap4\xc0N\\\x10l\xc0\xfb\x02\x00\x01Z\x00\x0bacces                                     sOrderxr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nl                                     oadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x0cw\b\x00\x00\x00\x10\x00\x00                                     \x00\x01t\x00\tTestRealmsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e                                     \x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw                                     \x0c\x00\x00\x00\x02?@\x00\x00\x00\x00\x00\x01t\x00\x03wzsxx\x00w\x01\x01q\x00~\                                     x00\x05x"

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。且后台重启以后页面没调转到登录页,说明记住我生效了。

上一篇:Python学习教程:Python3内置模块之Pickle和cPickle数据持久化方法小结


下一篇:json和pickle模块