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)