在传统的前后端分离模式中,我们通常是在请求头中增加一个请求头Authorization,它的值是一串加密的信息或者密钥,在后台通过对这个请求头值的读取,获取用户的信息。
而在这样的模式中,通常都是开发者自己设计的session或者加密(比如JWT)方式来读取和保存用户信息,而在shiro中,集成了权限控制和用户管理在它的session系统中,这就意味着我们只能通过他所规定的session+cookie来保存用户信息,在这种情况下,该以什么方式在前后端分离的项目中使用shiro?
通过资料的查询,和对shiro设计模式的解读,我发现shiro和servlet一样实在cookie中存储一个session会话的id然后在每次请求中读取该session的id并获取session,这样就可以获取指定session中储存的用户信息。
我们通过重写shiro中获取cookie中的sessionId的方法来获取请求头Authorization中的密钥,而密钥储存的便是登录是返回的sessionId,从而实现在前后端分离的项目中使用shiro框架。
参考代码
https://github.com/gemingyi/shiro_demo
我们来看核心代码
public class MySessionManager extends DefaultWebSessionManager {
//前端请求头传这个
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
public class RedisSessionDAO extends AbstractSessionDAO {
private static final long DEFAULT_SESSION_LIVE = 3600L;
private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro_redis_session:";
private long sessionLive = DEFAULT_SESSION_LIVE;
private String sessionKeyPrefix = DEFAULT_SESSION_KEY_PREFIX;
//使用的是spring-data-redis
private JedisConnectionFactory jedisConnectionFactory;
private byte[] StringToByte(String string) {
return string == null?null:string.getBytes();
}
//使用spring自带的序列工具类
private byte[] objectToByteArray(Object obj) {
return SerializationUtils.serialize(obj);
}
private Object byteArrayToObject(byte data[]) {
return SerializationUtils.deserialize(data);
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
RedisConnection connection = this.jedisConnectionFactory.getConnection();
Session session = null;
try {
session = (Session) this.byteArrayToObject(connection.get(this.getRedisSessionKey(sessionId)));
} catch (Exception e) {
e.printStackTrace();
}
connection.close();
return session;
}
@Override
public void update(Session session) {
this.saveSession(session);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
return;
}
RedisConnection connection = this.jedisConnectionFactory.getConnection();
try{
connection.del(this.getRedisSessionKey(session.getId()));
} catch (Exception e) {
e.printStackTrace();
}
connection.close();
}
@Override
public Collection getActiveSessions() {
Set allSessions = new HashSet<>();
RedisConnection connection = this.jedisConnectionFactory.getConnection();
try {
Set keys = connection.keys(this.objectToByteArray(this.sessionKeyPrefix + "*"));
if(keys != null && keys.size() > 0) {
for (byte[] key : keys) {
allSessions.add((Session) this.byteArrayToObject(connection.get(key)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
connection.close();
return allSessions;
}
private void saveSession(Session session) {
RedisConnection connection = this.jedisConnectionFactory.getConnection();
try {
byte[] key = this.getRedisSessionKey(session.getId());
byte[] value = this.objectToByteArray(session);
connection.setEx(key, sessionLive, value);
} catch (Exception e){
e.printStackTrace();
}
connection.close();
}
private byte[] getRedisSessionKey(Serializable sessionId) {
return this.StringToByte((this.sessionKeyPrefix + sessionId));
}
public void setSessionLive(long sessionLive) {
this.sessionLive = sessionLive;
}
public void setSessionKeyPrefix(String sessionKeyPrefix) {
this.sessionKeyPrefix = sessionKeyPrefix;
}
public void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
this.jedisConnectionFactory = jedisConnectionFactory;
}
}
自定义MyRealm继承AuthorizingRealm类,这个我就不贴出来了
配置shiro执行我们重写的类,下面是ShiroConfiguration类
@Configuration
public class ShiroConfiguration {
@Value("${shiro.redis.sessionLive}")
private long sessionLive;
@Value("${shiro.redis.sessionPrefix}")
private String sessionPrefix;
@Value("${shiro.redis.cacheLive}")
private long cacheLive;
@Value("${shiro.redis.cachePrefix}")
private String cachePrefix;
/**
* 凭证匹配器(密码加密)
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//加密的次数
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 定义Session ID生成管理器
* @return
*/
@Bean(name = "sessionIdGenerator")
public JavaUuidSessionIdGenerator sessionIdGenerator() {
JavaUuidSessionIdGenerator sessionIdGenerator = new JavaUuidSessionIdGenerator();
return sessionIdGenerator;
}
/**
* 自定义redisSessionDAO(session存放在redis中)
* @return
*/
@Bean(name = "redisSessionDAO")
public RedisSessionDAO redisSessionDAO(JavaUuidSessionIdGenerator sessionIdGenerator, @Qualifier("jedisConnectionFactory")JedisConnectionFactory jedisConnectionFactory) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator);
//session过期时间及前缀
redisSessionDAO.setSessionLive(sessionLive);
redisSessionDAO.setSessionKeyPrefix(sessionPrefix);
//注入jedisConnectionFactory
redisSessionDAO.setJedisConnectionFactory(jedisConnectionFactory);
return redisSessionDAO;
}
/**
* 自定义sessionManager
* @return
*/
@Bean(name = "sessionManager")
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionDAO(redisSessionDAO);
return mySessionManager;
}
@Bean(name = "myRealm")
public MyRealm myRealm() {
MyRealm myShiroRealm = new MyRealm();
// myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
myShiroRealm.setCachingEnabled(true);
return myShiroRealm;
}
@Bean(name = "securityManager")
public SecurityManager securityManager(SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean(name = "shirFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//注意过滤器配置顺序 不能颠倒
Map filterChainDefinitionMap = new LinkedHashMap();
//退出
filterChainDefinitionMap.put("/logout", "logout");
//匿名访问 跳转页面
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/userLogin", "anon");
filterChainDefinitionMap.put("/login", "anon");
//
filterChainDefinitionMap.put("/**", "authc");
//未认证 跳转页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 认证成功 跳转页面
// shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权 跳转页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
最后、看看Controller层登录方法
@RequestMapping(value = "/userLogin", method = RequestMethod.POST)
public Map ajaxLogin(User userInfo) {
Map result = new HashMap<>();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userInfo.getUserName(), userInfo.getPassword());
try {
subject.login(token);
result.putAll(ResponseEntity.responseSuccess(subject.getSession().getId()));
} catch (Exception e) {
result.put("code", CodeAndMsgEnum.ERROR.getcode());
result.put("msg", e.getMessage());
}
return result;
}
好了,关键代码就这些
下面我们看看,前端测试(我用的是postman)
Redis中session信息
参考文章