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