依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
主配置类
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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 org.springframework.data.redis.core.RedisTemplate;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Configuration
public class ShiroConfig {
/**
* hash散列次数,从配置文件中读取,或者也可以直接写死;自己决定,常用的为1024等
*/
@Value("${shiro.HashIterations}")
Integer hashIterations;
/**
* 创建Realm 用于读取数据库验证账号密码
* @param userDAO 访问数据库用的DAO
* @return
*/
@Bean
public CustomerRealm customerRealm(UserDAO userDAO) {
return new CustomerRealm(userDAO, hashIterations);
}
/**
* 创建安全管理器
*
* @param customerRealm
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CustomerRealm customerRealm,
RedisTemplate<Object, Object> redisTemplate) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//自定义Realm
defaultWebSecurityManager.setRealm(customerRealm);
//自定义session管理器
RedisSessionDao redisSessionDao = new RedisSessionDao(redisTemplate);
defaultWebSecurityManager.setSessionManager(new ShiroSessionManager(redisSessionDao));
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> map = new HashMap<>();
//不需要验证的资源
map.put("/user/reg**", "anon");
map.put("/user/login**", "anon");
map.put("/js/**", "anon");
map.put("/oauth/**", "anon");
map.put("/img/**", "anon");
//需要验证的资源
map.put("/**", "authc");
shiroFilterFactoryBean.setLoginUrl("/user/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
CustomerRealm
CustomerRealm为自定义Realm,需要继承AuthorizingRealm类,并实现其中的doGetAuthenticationInfo(认证)和doGetAuthorizationInfo(授权)方法。和网上搜到的做法略有不同,因为需要传入的参数较少,这里直接把一些初始化配置写在构造方法中,这样在ShiroConfig 中可以写得更简洁。其中会用到自定义的缓存管理器RedisShiroCache
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.List;
/**
* 自定义realm
*
* @author bx002
*/
@Slf4j
public class CustomerRealm extends AuthorizingRealm {
private final UserDAO userDAO;
public CustomerRealm(UserDAO userDAO, Integer hashIterations) {
this.userDAO = userDAO;
//修改凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//密码加密方式使用md5
credentialsMatcher.setHashAlgorithmName("md5");
//md5加密时使用的hash散列次数
credentialsMatcher.setHashIterations(hashIterations);
setCredentialsMatcher(credentialsMatcher);
//开启认证缓存 并设置缓存名称
setAuthenticationCachingEnabled(true);
setAuthenticationCacheName("authenticationCache_smarthome");
//开启授权缓存 并设置缓存名称
setAuthorizationCachingEnabled(true);
setAuthorizationCacheName("authorizationCache_smarthome");
//设置缓存管理器
//创建时会把 上方的两个名称传入 可以作为Redis的hash名称
setCacheManager(RedisShiroCache::new);
setCachingEnabled(true);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
List<String> roles = userDAO.findRoles(primaryPrincipal);
//添加角色信息
simpleAuthorizationInfo.addRoles(roles);
//还可以添加权限字符串
// simpleAuthorizationInfo.addStringPermission("*:*:*");
log.info("授权验证 {} {}", primaryPrincipal, roles);
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//登录验证
String principal = (String) token.getPrincipal();
//userDAO使用了Mybatis-plus来查询用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", principal).eq("available", 0);
User user = userDAO.selectOne(queryWrapper);
if (user != null) {
return new SimpleAuthenticationInfo(
user.getUsername(), user.getPassword(),
//解决盐序列化问题
new SimpleByteSource(user.getSalt())
, this.getName()
);
}
return null;
}
}
RedisShiroCache
自定义缓存类需要实现org.apache.shiro.cache.Cache接口,并实现其中的所有方法,方法应当操作Redis进行增删改查操作。由于我们使用了spring-boot-starter-data-redis依赖,工厂中直接有一个redisTemplate对象,可以用静态工具类获取
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.Set;
/**
* @author bx002
* @date 2020/11/21 14:18
*/
@Slf4j
public class RedisShiroCache<K, V> implements Cache<K, V> {
private String name;
private BoundHashOperations hash;
public RedisShiroCache(String s) {
//传入的缓存名称 作为hash名称使用
this.name = s;
}
public RedisShiroCache() {
}
@Override
public V get(K k) throws CacheException {
log.debug("Redis get {} {}", name, k);
return (V) getHash().get(k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
log.debug("Redis put {} > {} - {}", name, k, v);
getHash().put(k.toString(), v);
return v;
}
@Override
public V remove(K k) throws CacheException {
log.debug("remove {} {}", name, k);
V v = get(k);
getHash().delete(k.toString());
return v;
}
@Override
public void clear() throws CacheException {
log.debug("clear {}", name);
getRedisTemplate().delete(name);
}
@Override
public int size() {
return Math.toIntExact(getHash().size());
}
@Override
public Set<K> keys() {
return getHash().keys();
}
@Override
public Collection<V> values() {
return null;
}
private BoundHashOperations getHash() {
hash = hash == null ? getRedisTemplate().boundHashOps(name) : hash;
return hash;
}
private RedisTemplate getRedisTemplate() {
return (RedisTemplate) SpringContextUtil.getBean("redisTemplate");
}
}
获取redisTemplate的静态工具类
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring工具类,获取Spring上下文对象等
*
* @author Mr.Qu
* @since 2020/1/9 16:26
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextUtil.applicationContext == null) {
SpringContextUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
DefaultWebSecurityManager 安全管理器
前面我们在主配置类中直接简单的new 了一个安全管理器,并且通过setRealm方法设置Realm为我们的自定义Realm。
然后我们需要让Redis来接管Session,为此我们需要一个自定义的Session管理器,并将它对Session的管理指向Redis,为此我们还需要一个自定义SessionDAO,和上面一样,方法参数中加入了redisTemple以操作Redis。
如果不需要Redis接管Session,则setSessionManager方法直接传一个new DefaultWebSecurityManager()即可。
RedisSessionDao
自定义SessionDAO类 ,需要继承AbstractSessionDAO类 ,并实现其中方法对Redis进行增删改查操作,本例中把redisTemple绑定了一个hash key,以方便操作。需要注意doCreate方法的前两行,如果缺少则session将为一个空对象。
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
/**
* @author bx002
* @date 2020/11/24 12:13
*/
@Slf4j
public class RedisSessionDao extends AbstractSessionDAO {
private final BoundHashOperations<Object, Object, Object> hash;
public RedisSessionDao(RedisTemplate<Object, Object> redisTemplate) {
//绑定哈希
hash = redisTemplate.boundHashOps("Shiro_Session");
}
@Override
protected Serializable doCreate(Session session) {
//分配sessionId
final Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
//保存session并存储到Redis集群中
hash.put(sessionId, session);
log.debug("创建session {} -> {}", sessionId, session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
log.debug("读取session {}", sessionId);
return (Session) hash.get(sessionId);
}
@Override
public void update(Session session) throws UnknownSessionException {
Serializable sessionId = session.getId();
log.debug("更新session {} {}", sessionId, session);
hash.put(sessionId, session);
}
@Override
public void delete(Session session) {
Serializable id = session.getId();
log.debug("删除session {}", id);
hash.delete(id);
}
@Override
public Collection<Session> getActiveSessions() {
ArrayList<Session> sessions = new ArrayList<>();
Objects.requireNonNull(hash.values()).forEach(v -> {
sessions.add((Session) v);
});
return sessions;
}
}
ShiroSessionManager
自定义的SessionManager , 与前文类似,把大部分初始化配置写在了构造方法中。同时该类不需要交给工厂管理,所以选择直接在ShiroConfig中new出,而不注册为bean。
注意 new SimpleCookie(“sid”) 处修改sessionId的名称的操作,如果缺少可能导致前端获取到的SessionId一直变化的问题。
注意重写的方法 retrieveSession ,如果缺少会导致一次请求不必要地多次读取Redis的问题
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;
/**
* 自定义ShiroSession管理器
*
* @author bx002
* @date 2020/11/24 13:52
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
public ShiroSessionManager(RedisSessionDao redisSessionDao) {
//自定义shiro使用的Cookie key 避开与tomcat的sessionId的冲突
SimpleCookie sessionIdCookie = new SimpleCookie("sid");
sessionIdCookie.setMaxAge(-1);
sessionIdCookie.setPath("/");
sessionIdCookie.setHttpOnly(false);
//自定义cookie 中sessionId 的key
setSessionIdCookieEnabled(true);
setSessionIdCookie(sessionIdCookie);
//删除过期session
setDeleteInvalidSessions(true);
//设置session 过期时间
setGlobalSessionTimeout(60 * 60 * 1000);
setSessionValidationSchedulerEnabled(true);
//保存session到redis
setSessionDAO(redisSessionDao);
}
/**
* 重写该方法以解决一次请求中多次读取Redis的问题
*
* @param sessionKey
* @return
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) {
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;
}
}
redisTemplate对象注意事项
通常推荐把key和hashkey的序列化方式改为StringRedisSerializer,但是不要在本例的缓存和session中改动值的序列化方式(即使用默认的jdk方式),否则会在序列化或反序列化时出现错误。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
在其他地方希望使用Json作为序列化方式时,可以自行创建一个template
值序列化方式不要使用GenericJackson2JsonRedisSerializer,反序列化时会报错。
@Bean
public RedisTemplate<String, Object> redisJsonTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//String的序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericFastJsonRedisSerializer());
template.setHashValueSerializer(new GenericFastJsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
自定义Realm中用到的SimpleByteSource
仅用来解决盐的序列化问题
mport org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
/**
* @author bx002
* @date 2020/11/21 14:46
*/
public class SimpleByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public SimpleByteSource(byte[] bytes) {
this.bytes = bytes;
}
public SimpleByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public SimpleByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public SimpleByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public SimpleByteSource(File file) {
this.bytes = (new SimpleByteSource.BytesHelper()).getBytes(file);
}
public SimpleByteSource(InputStream stream) {
this.bytes = (new SimpleByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
public SimpleByteSource() {
}
}