spring boot 集成shiro和redis (絕不坑你)

spring boot 集成shiro和redis (絕不坑你)

最近兩天一直在弄shiro跟spring boot的整合,網上討論的很多,但是真正搞懂的人很少,都是抄抄抄,我現在就將我這兩天的所有結果分享給大家,供大家參考。
跟spring mvc 的集成很相像,但是也有很多不同的地方,主要體現在springboot 跟mvc的配置差異上。配置shiro,如果要跑起來,非常簡答,跟着網上說的來就可以了,但是,真正比較完美的配置,至少我在百度上面還沒有看到,身爲代碼潔癖的我,這是不能忍的。要配置shiro,基礎工具就是緩存,沒有緩存,你的配置將只能是一個玩具,一個在內存中玩玩而已的工具,我採用的redis。

一、spring boot 跟redis 的結合

1、導入。

首先是pom包,需要

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

沒錯,就這一個就可以了,其他的都不需要。
注:我的sprint-boot 版本是2.0.4.RELEASE

2、配置

請查看org.springframework.boot.autoconfigure.data.redis.RedisProperties 源碼,那裏有全部的配置說明,這就是starter的好處,直接看源碼遠比你網上百度要好千倍萬倍。這也是經驗之談了。配置好了redis之後,我們進入下一步。

3、使用

請查看org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration源碼,那裏面註冊了兩個bean,redisTemplate 和 stringRedisTemplate,那麼意味着,你可以直接使用這兩個template了,即可以直接使用了。至於如何繼承到自己的項目中,你直接註冊一個bean,然後將get、set、put、delete等方法通過redisTemplate實現了即可,如果看不懂,請移步,鞏固基礎。

二、redis實現shiro的cacheManager。

1、導入

首先還是要導入pom包,需要

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

我這裏使用1.4.0的,是我目前能看到的最新的

2、繼承cacheManager

shiro要想使用cacheManager,就必須實現org.apache.shiro.cache.CacheManager類,才能與shiro集成。我這裏直接繼承了AbstractCacheManager,需要實現createCache方法。我的實現類如下:

public class ShiroRedisCacheManager extends AbstractCacheManager {

    private RedisTemplate<byte[],byte[]> redisTemplate;

    public ShiroRedisCacheManager(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    //爲了個性化配置redis存儲時的key,我們選擇了加前綴的方式,所以寫了一個帶名字及redis操作的構造函數的Cache類
    @Override
    protected Cache createCache(String name) throws CacheException {
        return new ShiroRedisCache(redisTemplate,name);
    }
}

3、繼承Cache

在cacheManage中,要實現createCache方法,就需要返回一個org.apache.shiro.cache.Cache

public class ShiroRedisCache<K,V> implements Cache<K,V> {
    private RedisTemplate redisTemplate;
    private String prefix = "shiro_redis";

    public String getPrefix() {
        return prefix+":";
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public ShiroRedisCache(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    public ShiroRedisCache(RedisTemplate redisTemplate,String prefix){
        this(redisTemplate);
        this.prefix = prefix;
    }

    @Override
    public V get(K k) throws CacheException {
        if (k == null) {
            return null;
        }
        byte[] bytes = getBytesKey(k);
        return (V)redisTemplate.opsForValue().get(bytes);

    }

    @Override
    public V put(K k, V v) throws CacheException {
        if (k== null || v == null) {
            return null;
        }

        byte[] bytes = getBytesKey(k);
        redisTemplate.opsForValue().set(bytes, v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        if(k==null){
            return null;
        }
        byte[] bytes =getBytesKey(k);
        V v = (V)redisTemplate.opsForValue().get(bytes);
        redisTemplate.delete(bytes);
        return v;
    }

    @Override
    public void clear() throws CacheException {
        redisTemplate.getConnectionFactory().getConnection().flushDb();

    }

    @Override
    public int size() {
        return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();
    }

    @Override
    public Set<K> keys() {
        byte[] bytes = (getPrefix()+"*").getBytes();
        Set<byte[]> keys = redisTemplate.keys(bytes);
        Set<K> sets = new HashSet<>();
        for (byte[] key:keys) {
            sets.add((K)key);
        }
        return sets;
    }

    @Override
    public Collection<V> values() {
        Set<K> keys = keys();
        List<V> values = new ArrayList<>(keys.size());
        for(K k :keys){
            values.add(get(k));
        }
        return values;
    }

    private byte[] getBytesKey(K key){
        if(key instanceof String){
            String prekey = this.getPrefix() + key;
            return prekey.getBytes();
        }else {
            return ObjectUtil.serialize(key);
        }
    }

}

這沒什麼好說的,直接copy即可

注:我的所有工具類都用hutool,很全很強大,也不依賴其他jar,專治強迫症,極力推薦。

至此,redis準備工作已經完成。

三、開始shiro之旅

這一塊,網上配置很多,也有很多糟粕,我不敢說我的就是對的,但是,我一定是仔細分析,認真思考過的,有任何問題,大家一起討論。

1、配置yml

hayek:
  shiro:
    # shiro redis緩存時長,默認1800秒
    expireIn: 1800
    # session 超時時間,默認1800000毫秒
    sessionTimeout: 1800000
    # rememberMe cookie有效時長,默認30天
    cookieTimeout: 2592000
    # 免認證的路徑配置,如靜態資源,druid監控頁面,註冊頁面,驗證碼請求等
    anonUrl: /css/**,/js/**,/fonts/**,/adminres/**,/img/**
    # 登錄 url
    loginUrl: /login
    # 登錄成功後跳轉的 url
    successUrl: /admin/index
    # 登出 url
    logoutUrl: /logout
    # 未授權跳轉 url
    unauthorizedUrl: /error
    # session的id名稱
    sessionIdName: hayek.session.id

2、讀取配置

這裏就是一個Properties的配置讀取而已,添加 @ConfigurationProperties(prefix = “hayek.shiro”)註解,將配置讀取出來即可,不貼源碼了。

3、配置shiro

/**
 * Shiro 配置類
 * shiro的核心配置類 shiro的所有初始化bean都在這個類中操作,各個bean我在下面都會做詳細的註釋,幫助理解
 * @author super小靖
 */
@Configuration
public class ShiroConfig {
    /**
     * 部分配置文件,詳細見application.yml
     */
    @Autowired
    private ShiroProperties shiroProperties;

    /**
     * shiro的攔截器,在spring mvc中也有相同的配置,這裏不再多說
     * @author Super小靖
     * @date 2018/8/29
     * @param securityManager
     * @return
     **/
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 設置 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登錄的 url
        shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl());
        // 登錄成功後跳轉的 url
        shiroFilterFactoryBean.setSuccessUrl(shiroProperties.getSuccessUrl());
        // 未授權 url
        shiroFilterFactoryBean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());
        // 這裏配置授權鏈,跟mvc的xml配置一樣
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 設置免認證 url
        String[] anonUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(shiroProperties.getAnonUrl(), ",");
        for (String url : anonUrls) {
            filterChainDefinitionMap.put(url, "anon");
        }
        // 配置退出過濾器,其中具體的退出代碼 Shiro已經替我們實現了
        filterChainDefinitionMap.put(shiroProperties.getLogoutUrl(), "logout");
        // 除上以外所有 url都必須認證通過纔可以訪問,未通過認證自動訪問 LoginUrl
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    /**
     * 配置各種manager,跟xml的配置很像,但是,這裏有一個細節,就是各個set的次序不能亂
     * @author Super小靖
     * @date 2018/8/29
     * @param realm
     * @return
     **/
    @Bean
    @DependsOn({"shiroRealm"})
    public SecurityManager securityManager(ShiroRealm realm,RedisTemplate<Object, Object> template) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置 rememberMeCookie 查看源碼可以知道,這裏的rememberMeManager就僅僅是一個賦值,所以先執行
        securityManager.setRememberMeManager(rememberMeManager());
        // 配置 緩存管理類 cacheManager,這個cacheManager必須要在前面執行,因爲setRealm 和 setSessionManage都有方法初始化了cachemanager,看下源碼就知道了
        securityManager.setCacheManager(cacheManager(template));
        // 配置 SecurityManager,並注入 shiroRealm 這個跟springmvc集成很像,不多說了
        securityManager.setRealm(realm);
        // 配置 sessionManager
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    /**
     * 生成一個ShiroRedisCacheManager 這沒啥好說的
     * @author Super小靖
     * @date 2018/8/29
     * @param template
     * @return
     **/
    private ShiroRedisCacheManager cacheManager(RedisTemplate template){
        return new ShiroRedisCacheManager(template);
    }

    /**
     * 這是我自己的realm 我自定義了一個密碼解析器,這個比較簡單,稍微跟一下源碼就知道這玩意
     * @param matcher
     * @param userService
     * @return
     */
    @Bean
    @DependsOn({"hashedCredentialsMatcher"})
    public ShiroRealm shiroRealm(HashedCredentialsMatcher matcher, SysUserService userService) {
        // 配置 Realm,需自己實現
        return new ShiroRealm(matcher,userService);
    }

    /**
     * 密碼解析器 有好幾種,我這是MD5 1024次加密
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher createMatcher(){
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DefaultConstants.HASH_ALGORITHM);
        matcher.setHashIterations(DefaultConstants.HASH_INTERATIONS);
        return matcher;
    }
    /**
     * rememberMe cookie 效果是重開瀏覽器後無需重新登錄
     *
     * @return SimpleCookie
     */
    private SimpleCookie rememberMeCookie() {
        // 這裏的Cookie的默認名稱是 CookieRememberMeManager.DEFAULT_REMEMBER_ME_COOKIE_NAME
        SimpleCookie cookie = new SimpleCookie(CookieRememberMeManager.DEFAULT_REMEMBER_ME_COOKIE_NAME);
        // 是否只在https情況下傳輸
        cookie.setSecure(false);
        // 設置 cookie 的過期時間,單位爲秒,這裏爲一天
        cookie.setMaxAge(shiroProperties.getCookieTimeout());
        return cookie;
    }

    /**
     * cookie管理對象
     *
     * @return CookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie 加密的密鑰
        cookieRememberMeManager.setCipherKey(Base64.decode("ZWvohmPdUsAWT3=KpPqda"));
        return cookieRememberMeManager;
    }

    /**
     * 用於開啓 Thymeleaf 中的 shiro 標籤的使用
     * 我沒用過,不作評論
     * @return ShiroDialect shiro 方言對象
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }


    /**
     * session 管理對象
     *
     * @return DefaultWebSessionManager
     */
    private DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 設置session超時時間,單位爲毫秒
        sessionManager.setGlobalSessionTimeout(shiroProperties.getSessionTimeout());
        sessionManager.setSessionIdCookie(new SimpleCookie(shiroProperties.getSessionIdName()));
        // 網上各種說要自定義sessionDAO 其實完全不必要,shiro自己就自定義了一個,可以直接使用,還有其他的DAO,自行查看源碼即可
        sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        return sessionManager;
    }
}

註釋很詳細,就不詳細說了,自己看註釋

4、其他注意事項

shiro自定義的filter有如下幾種

anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);

使用時最好查看其對應的Filter類,然後根據源碼來看其功能會簡單好多。
我們項目中用了authc 這個filter,對應的是FormAuthenticationFilter,這個filter不需要自己寫post登錄接口,內部會自動登錄處理,但是表單的name必須是對應如下

public static final String DEFAULT_USERNAME_PARAM = "username";
public static final String DEFAULT_PASSWORD_PARAM = "password";
public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";

否則會識別失敗。

5、controller部分

@RequestMapping("/")
public String enter(){
    return "redirect:/login";
}
/**
 * 登錄get方法獲取頁面
 * @return
 */
@RequestMapping(value = "login",method = {RequestMethod.GET})
public String login() {
    Object principal = SecurityUtils.getSubject().getPrincipal();
    // 如果已經登錄,則跳轉到歡迎首頁
    if(principal != null){
        return "redirect:/admin/index";
    }
    return "login";
}
/**
 * 登錄用戶名密碼錯誤之後會調用
 * @return
 */
@RequestMapping(value = "login",method = {RequestMethod.POST})
public String loginError(HttpServletRequest request, Model model) {
    Object attribute = request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
    if(attribute != null){
        model.addAttribute("error", ConfigUtil.getError("login-001"));
    }
    return "login";
}

如果在rememberMe的過程中出現重定向循環的問題,請訪問https://blog.csdn.net/nsrainbow/article/details/36945267/ 查找原因

全文完。
spring boot 集成shiro和redis(我的blog)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章