SpringBoot中整合Shiro+Redis实现认证、授权缓存及Session共享 踩坑笔记

依赖

<!--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() {
    }
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值