shiro是我目前用的项目的一个鉴权框架,也是apache基金会的,用来处理登录,鉴权这部分。下面是官网的介绍页:
网上一个用XML文件配置的,这里我用注解的方式来配置跟他做一个对比
一般来说对于登录,鉴权,身份验证,session管理其实不管什么项目都是一个不容忽视的部分。采用shiro就是希望有个对用的框架来直接使用,而不是自己去修改,很多时候,项目紧急容不得你“慢悠悠”的造轮子。而且shiro的使用还算方便,这里主要是讨论shiro如何去设置多种方式登录。
一 问题:登录可以支持多种方式登录,账户密码,手机验证码,或者微信钉钉的自动登录?
这其实是一个很现实的问题,尤其是现在很多平台,开放权限可以通过大平台自身的鉴权验证,来让用户用第三方账号来登录使用。
shiro支持多种方式,除了验证方式不同,比如有的是微信扫码,微信服务器返回验证信息的第三方登录,有的是自己数据库的用户密码验证。甚至可以来源不同的数据库,只要你能访问这个数据库信息即可。
二 实操:如何设置多种方式?
(1)导入依赖shiro
首先你要把shiro框架,弄到你项目来进行配置maven项目依赖(gradle什么的都可以,就是对应的文件和写法有区别),由shiro来负责执行身份验证,授权,加密和会话管理。
首先我从网上看到一个配置代码,贴了过来:
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<!-- spring整合shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- ehcache-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
我们自己的项目,采用的是一个shiro的开源插件,由于我们自己包装了一层,可能没什么推荐价值,也发一下
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro.redis.version}</version>
</dependency>
(2)shiro拦截配置
这里我看有的地方可能还要配置web.xml,这里我就跳过这一步,说一下shiro的配置,这时候先拿网友配置文件的方式
XML配置文件
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
5
6
7 <!-- 配置ehCache缓存支持-->
8 <bean name="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
9 <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
10 </bean>
11
12
13 <!-- 自定义Realm-->
14 <bean name="userRealm" class="com.newer.web.shiro.MyRealm" >
15
16 <!--注入加密算法类 -->
17 <property name="credentialsMatcher">
18
19 <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
20 <!--加密算法名 -->
21 <property name="hashAlgorithmName" value="MD5"/>
22 <!--加密次数 -->
23 <property name="hashIterations" value="6"/>
24 </bean>
25
26 </property>
27
28
29 </bean>
30
31
32 <!-- shiro安全管理器-->
33 <bean name="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
34 <property name="cacheManager" ref="cacheManager"></property>
35 <property name="realm" ref="userRealm"></property>
36 </bean>
37
38
39 <!-- 管理shiro bean的生命周期 -->
40 <bean name="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
41
42
47
48 <!--shiro核心过滤器配置-->
49 <bean name="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
50 <property name="securityManager" ref="securityManager"/>
51 <property name="loginUrl" value="/login.jsp"></property> <!-- 没有认证 跳转的登录连接 -->
52 <property name="unauthorizedUrl" value="/unauthorized.jsp"></property> <!-- 没有访问权限 跳转的页面 -->
82 <!--这里使用 从数据库查询出来要拦截的url集合 注入到这个对象中 实现拦截 -->
83 <property name="filterChainDefinitionMap" ref="map"/>
86 </bean>
87
88
89 <!--实例工厂方法 将执行shiroFilterUtils类的build的方法 -->
90 <bean name="map" factory-bean="shiroFilterUtils" factory-method="build"/>
91
92 <!--这个类将从数据库中查询要拦截的url信息-->
93 <bean name="shiroFilterUtils" class="com.newer.util.ShiroFilterUtils"/>
97 </beans>
然后我用注解的方式来完成的配置来对比(因为缓存组件彼此都不同,他用的是ehcache,我用的是redis之类的一些细节不同,但是大体思路上主要的部分都是相同的)
package com.lxzl.ams.app.config.shiro.common;
import com.lxzl.ams.app.config.shiro.account.AccountShiroRealm;
import com.lxzl.ams.app.config.shiro.email.EmailShiroRealm;
import com.lxzl.ams.app.config.shiro.filter.ShiroLoginFilter;
import com.lxzl.ams.app.config.shiro.phone.DingTaskShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.FeiShuWorkShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.PhoneShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.WeChatWorkShiroRealm;
import com.lxzl.ams.app.config.shiro.session.OnlineWebSessionManager;
import com.lxzl.ams.app.config.shiro.session.SpringSessionValidationScheduler;
import com.lxzl.ams.common.config.RedisConfig;
import com.lxzl.ams.common.core.CommonConstants;
import com.lxzl.skull.util.StringUtil;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.*;
/**
* Shiro 配置类
*/
@Configuration
class ShiroConfiguration {
@Value("${server.servlet.session.timeout}")
private Long sessionTimeOut;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
@Autowired
private RedisConfig redisConfig;
@Value("${server.servlet.session.timeout}")
private Long sessionTimeout;
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* <p>
* 部分过滤器可指定参数,如perms,roles
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters
//将自定义 的权限验证失败的过滤器ShiroFilterFactoryBean注入shiroFilter
//filters.put("perms", new ShiroPermissionsFilter());
// filters.put("authc", new AjaxPermissionsAuthorizationFilter());
filters.put("authc", new ShiroLoginFilter());
// 必须设置SecuritManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/ams/403");
// 权限控制map.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
// filterChainDefinitionMap.put("/user/logout", "anon");
filterChainDefinitionMap.put("/ams/403", "anon");
filterChainDefinitionMap.put("/ams/login", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/user/logout", "anon");
filterChainDefinitionMap.put("/ws/**", "anon");
//允许访问静态资源a
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filterChainDefinitionMap.put("/kaptcha/getKaptchaImage**", "anon");
//允许访问健康检查
filterChainDefinitionMap.put("/zabbix/service/status/monitor", "anon");
// 从数据库获取所有的权限
/* Map<String, String> map = sysMenuSupport.selectPermsAll();
filterChainDefinitionMap.putAll(map);*/
//filterChainDefinitionMap.putAll(SpringUtils.getBean(SysMenuService.class).selectPermsAll());
/* if(!CollectionUtil.isEmpty(map)){
filterChainDefinitionMap.putAll(map);
}*/
//current_Permission?.getUrl(),"perms[" + current_Permission?.getPermission() + "]"
//filterChainDefinitionMap.put("manage-system/admin","perms[user:pageUser]");
// 允许访问健康检查
filterChainDefinitionMap.put("/zabbix/service/status/monitor", "anon");
// filterChainDefinitionMap.put("/user/**","roles[45,46]");
//过滤链定义,从上向下顺序执行,一般将放在最为下边
filterChainDefinitionMap.put("/**", "authc");
//authc表示需要验证身份才能访问,还有一些比如anon表示不需要验证身份就能访问等。
logger.info("拦截器链:" + filterChainDefinitionMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
ThreadContext.bind(securityManager);
//设置realm.
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
//添加多个Realm
realms.add(accountShiroRealm());
realms.add(phoneShiroRealm());
realms.add(emailShiroRealm());
realms.add(weChatWorkShiroRealm());
realms.add(feiShuWorkShiroRealm());
realms.add(dingTaskShiroRealm());
securityManager.setRealms(realms);
//注入记住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
logger.info("创建shiro redisManager,连接Redis..URL= " + redisConfig.redisStandaloneConfiguration().getHostName() + ":" + redisConfig.redisStandaloneConfiguration().getPort());
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisConfig.redisStandaloneConfiguration().getHostName());
redisManager.setPort(redisConfig.redisStandaloneConfiguration().getPort());
// redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(timeout);
if (StringUtil.isNotBlank(password)) {
redisManager.setPassword(password);
}
return redisManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
* @return
*/
// public RedisCacheManager cacheManager() {
// logger.info("创建RedisCacheManager...");
// RedisCacheManager redisCacheManager = new RedisCacheManager();
// redisCacheManager.setRedisManager(redisManager());
// return redisCacheManager;
// }
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setKeyPrefix(CommonConstants.APP_SHIRO_SESSION);
redisSessionDAO.setExpire((int) (sessionTimeOut / 1000));
return redisSessionDAO;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public OnlineWebSessionManager sessionManager() {
OnlineWebSessionManager sessionManager = new OnlineWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
// 设置全局session超时时间
sessionManager.setGlobalSessionTimeout(sessionTimeOut);
// 相隔多久检查一次session的有效性
sessionManager.setSessionValidationInterval(sessionTimeOut);
// 删除过期的session
sessionManager.setDeleteInvalidSessions(true);
// 是否定时检查session
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdCookieEnabled(true);
SimpleCookie cookie = new SimpleCookie(UUID.randomUUID().toString());
cookie.setHttpOnly(true);
cookie.setMaxAge(60 * 60 * 1000);
sessionManager.setSessionIdCookie(cookie);
return sessionManager;
}
/**
* cookie对象;
*
* @return
*/
@Bean
public SimpleCookie rememberMeCookie() {
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
/**
* cookie管理对象;记住我功能
*
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
/**
* 系统自带的Realm管理,主要针对多realm
*/
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
//自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public AccountShiroRealm accountShiroRealm() {
AccountShiroRealm accountShiroRealm = new AccountShiroRealm();
accountShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());//设置解密规则
return accountShiroRealm;
}
@Bean
public PhoneShiroRealm phoneShiroRealm() {
PhoneShiroRealm phoneShiroRealm = new PhoneShiroRealm();
//phoneShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());//设置解密规则
return phoneShiroRealm;
}
@Bean
public WeChatWorkShiroRealm weChatWorkShiroRealm() {
WeChatWorkShiroRealm weChatWorkShiroRealm = new WeChatWorkShiroRealm();
weChatWorkShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
return weChatWorkShiroRealm;
}
@Bean
public FeiShuWorkShiroRealm feiShuWorkShiroRealm() {
FeiShuWorkShiroRealm feiShuWorkShiroRealm = new FeiShuWorkShiroRealm();
feiShuWorkShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
return feiShuWorkShiroRealm;
}
@Bean
public DingTaskShiroRealm dingTaskShiroRealm() {
DingTaskShiroRealm dingTaskShiroRealm = new DingTaskShiroRealm();
dingTaskShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
return dingTaskShiroRealm;
}
@Bean
public EmailShiroRealm emailShiroRealm() {
EmailShiroRealm emailShiroRealm = new EmailShiroRealm();
return emailShiroRealm;
}
//因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public AllowAllCredentialsMatcher allowAllCredentialsMatcher() {
AllowAllCredentialsMatcher allowAllCredentialsMatcher = new AllowAllCredentialsMatcher();
return allowAllCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* 开启 权限注解
* Controller才能使用@RequiresPermissions
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 自定义sessionFactory调度器
*/
@Bean
public SpringSessionValidationScheduler sessionValidationScheduler() {
SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler();
// 设置会话验证调度器进行会话验证时的会话管理器
sessionValidationScheduler.setSessionManager(sessionManager());
return sessionValidationScheduler;
}
}
实际上从这里大家也可以看出,注解和配置文件差不多,你配置文件里面配置一个bean和我写一个@Bean注解其实是相同的
(3)配置多种登录方式
实际上你细心看我的配置类,你会发现有
//添加多个Realm
realms.add(accountShiroRealm());
realms.add(phoneShiroRealm());
realms.add(emailShiroRealm());
realms.add(weChatWorkShiroRealm());
realms.add(feiShuWorkShiroRealm());
realms.add(dingTaskShiroRealm());
securityManager.setRealms(realms);
其实这些realm就是,shiro里面所说的“域”,也就是多种方式,shiro允许设置多个realm验证,一个不行就下一个,这几个配置的realm又成功的,那就登陆成功。在上面配置文件,我就add了不少realm,每个realm对应一个具体的验证业务逻辑。
然后我们就看一下realm我只怎么写的
package com.lxzl.ams.app.config.shiro.phone;
import com.lxzl.ams.app.config.shiro.common.UserToken;
import com.lxzl.ams.common.core.option.LoginType;
import com.lxzl.ams.common.dto.user.request.LoginPhoneParam;
import com.lxzl.ams.common.dto.user.response.UserLoginResponse;
import com.lxzl.ams.service.sysMenu.support.SysMenuSupport;
import com.lxzl.ams.service.user.UserService;
import com.lxzl.skull.exception.BusinessException;
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.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
public class PhoneShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(PhoneShiroRealm.class);
@Override
public String getName() {
return LoginType.PHONE.toString();
}
@Autowired
private UserService userService;
@Autowired
private SysMenuSupport sysMenuSupport;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("开始手机号短信身份认证..");
UserToken userToken = (UserToken) token;
String phone = userToken.getUsername();
LoginPhoneParam loginPhoneParam = new LoginPhoneParam();
String verifyCode = "";
if (((UserToken) token).getPassword() != null) {
verifyCode = new String(((UserToken) token).getPassword());
}
loginPhoneParam.setPhone(phone);
loginPhoneParam.setVerifyCode(verifyCode);
UserLoginResponse userLoginResponse = null;
//这里就是自己定义的具体验证逻辑,这里是验证手机验证码的service,你可以根据情况用不同的业务验证逻辑来完成
try {
userLoginResponse = (UserLoginResponse) userService.quickLoginPhone(loginPhoneParam, userToken.getIp()).getData();
} catch (BusinessException e) {
e.printStackTrace();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userLoginResponse, verifyCode, getName());
logger.info("返回手机号短信认证信息:" + authenticationInfo);
return authenticationInfo;
}
//当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("开始手机号短信权限授权");
return sysMenuSupport.checkPermission(principals);
}
}
这样从第二步的配置,到第三部具体一个“域”realm的编码,你差不多对于如何配置多个realm组成的shiro使用了,当然可有很多遗漏,你可以通过文档和其他人的文章进行对比学习。