实现shiro多方式登录系统
致谢
首先感谢张开涛大神,写了很多有关shiro的博客,内容详实并佐以实例,使鄙人可以很快上手使用shiro,在这里给出大神的跟我学shiro的博客,方便自己跟他人浏览学习。
前言
shiro的确很强大,可以在页面使用标签,也可以在控制层以注解的方式进行访问控制,只是做过网站的都知道,我们往往需要实现多种方式的登陆,例如:手机验证码登陆,QQ,微信登陆,等等,这个时候原生的shiro就无法达到这个功能了,作为一名java开发人员,最熟悉的就是封装/继承/多态了,OK,以手机验证码登陆为例,我们自己实现吧!
正文
理论
要进行多方式登陆
1.首先要区分用户使用的登陆方式
2.然后根据登陆方式选择我们注册到shiro中的某个匹配当前登陆方式的Realm,每个Realm中都能用来获取用户的认证信息以及权限信息,并根据这些信息判断用户是否有权利登陆系统以及访问哪些功能,那么就至少需要有两个以上的Realm提供给shiro的ModularRealmAuthenticator(模块化用户认证器)
3.shiro提供了三种策略来进行多Realm下认证:AllSuccessfulStrategy(全部匹配策略),AtLeastOneSuccessfulStrategy(至少一个匹配),FirstSuccessfulStrategy(第一个匹配),默认使用AtLeastOneSuccessfulStrategy,这些策略并符合我们的需要,因此重写ModularRealmAuthenticator
下面,了解一下需要继承的类/接口,以及需要重写的方法:
UsernamePasswordToken
此类保存了用户的登陆信息,但是不足以满足多登陆方式的要求,因而需要进行一些调整,增加一个loginType属性,用来保存用户的登陆方式
FormAuthenticationFilter.createToken(ServletRequest request, ServletResponse response)
此类是用户提交表单进行登陆认证的过滤器,此方法是用来以为用户提交的信息合成一个Token(令牌),并拿着这个令牌去判断信息是否匹配
ModularRealmAuthenticator.doMultiRealmAuthentication(Collection realms, AuthenticationToken token)
此类是模块化凭证匹配器此方法是用来在注入的realms中按照指定的策略,逐个获取保存在系统中认证信息并整合在一起,返回这个整合后的信息
AuthenticatingRealm.supports(AuthenticationToken token)
此类是认证信息匹配器,此方法用来判断当前用户提交的登陆信息是否可以被校验,返回true则会使用该类的实现类进行匹配,返回false则跳过
代码
创建UsernamePasswordLoginTypeToken,继承UsernamePasswordToken
/**
* Created by Lancelot on 2017/3/17.
* 重写{@link #UsernamePasswordToken},增加{@link #loginType}属性,该属性是在登陆界面form表单中传递过来的,定义了用户使用的登陆类型
*/
public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken {
private static final long serialVersionUID = 7134536615448037793L;
/**
*登陆类型
*/
private String loginType;
public UsernamePasswordLoginTypeToken(String username, String password, boolean rememberMe, String host, String loginType) {
super(username, password, rememberMe, host);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
创建MyFormAuthenticationFilter,继承FormAuthenticationFilter,并重写createToken():
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
public static final String DEFAULT_LOGIN_TYPE_PARAM = "loginType";
private boolean kickOutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
private int maxSession = 1; //同一个帐号最大会话数 默认1
private CacheManager cacheManager;
private SessionManager sessionManager;
private String kickOutSessionCacheName;
private Cache<String, Deque<Serializable>> cache;
private ReentrantLock reentrantLock = new ReentrantLock();
private String loginTypeParamName = DEFAULT_LOGIN_TYPE_PARAM;
@PostConstruct
public void init() {
this.cache = cacheManager.getCache(kickOutSessionCacheName);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//如果其他过滤器已经,验证失败了,则禁止登陆,不再进行身份验证
if (request.getAttribute(getFailureKeyAttribute()) != null) {
return true;
}
return super.onAccessDenied(request, response, mappedValue);
}
/**
* 重写登陆成功后的处理方法,使其跳转到指定的页面,这里是successUrl
*
* @param token 令牌
* @param subject 用户信息
* @param request 请求
* @param response 响应
* @return true 继续过滤,false 跳过之后的过滤;
* @throws Exception 异常
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
reentrantLock.lock();
Deque<Serializable> deque = cache.get(username);
if (deque == null) {
deque = new LinkedList<>();
}
//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId)) {
deque.push(sessionId);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickOutSessionId;
if (kickOutAfter) { //如果踢出后者
kickOutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickOutSessionId = deque.removeLast();
}
try {
Session kickOutSession = sessionManager.getSession(new DefaultSessionKey(kickOutSessionId));
if (kickOutSession != null) {
//设置会话的 kickOut 属性表示踢出了
kickOutSession.setAttribute("kickOut", true);
}
} catch (Exception e) {//ignore exception
}
}
cache.put(username, deque);
reentrantLock.unlock();
WebUtils.getAndClearSavedRequest(request);
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
return true;
}
@Override
protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
request.setAttribute(getFailureKeyAttribute(), ae);
}
/**
* 重写该方法,为了将loginType参数保存到token中
*
* @param request 请求
* @param response 响应
* @return
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
String loginType = getLoginType(request);
return createToken(username, password, request, response, loginType);
}
private AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response, String loginType) {
boolean rememberMe = isRememberMe(request);
String host = getHost(request);
return createToken(username, password, rememberMe, host, loginType);
}
private AuthenticationToken createToken(String username, String password, boolean rememberMe, String host, String loginType) {
return new UsernamePasswordLoginTypeToken(username, password, rememberMe, host, loginType);
}
private String getLoginType(ServletRequest request) {
return WebUtils.getCleanParam(request, getLoginTypeParamName());
}
.....省略getter/setter方法
}
创建MyModularRealmAuthenticator,继承自ModularRealmAuthenticator,重写doMultiRealmAuthentication()方法
/**
* Created by Lancelot on 2017/3/17.
* 重写模块化用户验证器,根据登录界面传递的loginType参数,获取唯一匹配的realm
*/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
private final Logger log = LoggerFactory.getLogger(MyModularRealmAuthenticator.class);
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
Realm uniqueRealm = getUniqueRealm(realms, token);
if (uniqueRealm == null) {
throw new UnsupportedTokenException("没有匹配类型的realm");
}
return uniqueRealm.getAuthenticationInfo(token);
}
/**
* 判断realms是否匹配,并返回唯一的可用的realm,否则返回空
*
* @param realms realm集合
* @param token 登陆信息
* @return 返回唯一的可用的realm
*/
private Realm getUniqueRealm(Collection<Realm> realms, AuthenticationToken token) {
for (Realm realm : realms) {
if (realm.supports(token)) {
return realm;
}
}
log.error("一个可用的realm都没有找到......");
return null;
}
}
创建UserRealm,继承自AuthorizingRealm,并重写里面的三个方法
/**
* Created by Lancelot on 2017/3/17.
* 从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作,可以把 Realm 看成 DataSource,即安全数据源。
* 从数据库中获取认证信息及授权信息
*/
public class UserRealm extends AuthorizingRealm {
/**
* 用户数据DAO
*/
@Autowired
private BiUserMapper userDao;
/**
* 支持的登陆类型
*/
private String supportedLoginType;
/**
* 授权验证
*
* @param principals 认证人
* @return 授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userDao.findRoles(username));
authorizationInfo.setStringPermissions(userDao.findPermissions(username));
return authorizationInfo;
}
/**
* 用户认证
*
* @param token 令牌
* @return 认证信息
* @throws AuthenticationException 认证失败
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
BiUser user = userDao.selectByLoginName(username);
if (user == null) {
throw new UnknownAccountException();//没找到帐号
}
if (user.getLockTime() != null && user.getLockTime().after(new Date())) {
throw new LockedAccountException(); //帐号锁定
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
return new SimpleAuthenticationInfo(
user.getLoginName(), //用户名
user.getLoginPwd(), //密码
getName() //realm name
);
}
public boolean supports(AuthenticationToken token) {
if (token instanceof UsernamePasswordLoginTypeToken) {
UsernamePasswordLoginTypeToken usernamePasswordLoginTypeToken = (UsernamePasswordLoginTypeToken) token;
return getSupportedLoginType().equals(usernamePasswordLoginTypeToken.getLoginType());
}
return false;
}
public String getSupportedLoginType() {
return supportedLoginType;
}
/**
*spring注入
*/
public void setSupportedLoginType(String supportedLoginType) {
this.supportedLoginType = supportedLoginType;
}
}
创建VerifyCodeRealm,继承自UserRealm,重写里面的两个方法
/**
* Created by Lancelot on 2017/3/16.
* 使用验证码登陆
*/
public class VerifyCodeRealm extends UserRealm {
@Autowired
private BiUserMapper userDao;
@Autowired
private ShiroCacheUtils shiroCacheUtils;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
BiUser user = userDao.selectByLoginName(username);
if (user == null) {
throw new UnknownAccountException();//没找到帐号
}
if (user.getLockTime() != null && user.getLockTime().after(new Date())) {
throw new LockedAccountException(); //帐号锁定
//从缓存中跟用户名取出验证码信息(验证码+到期时间)
CaptchaVo captcha = shiroCacheUtils.getCaptcha(username);
//判断验证码是否过期,如果过期,从缓存中移除并抛出一个自定义异常
if (captcha != null && captcha.isExpired()) {
shiroCacheUtils.clearUserCaptcha(username);
throw new CaptchaExpiredException("验证码已过期");
}
//验证码不存在,抛出一个自定义异常
if (captcha == null) {
throw new AuthenticationException("尚未发送验证码,请先获取");
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
return new SimpleAuthenticationInfo(
user.getLoginName(), //用户名
captcha.getCaptcha(), //验证码
getName() //realm name
);
}
/**
*重写断言验证码是否一致,为了方便区别错误原因,抛出一个自定义异常
*/
@Override
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
try {
super.assertCredentialsMatch(token, info);
} catch (IncorrectCredentialsException e) {
throw new IncorrectCaptchaException();
}
}
}
附上spring-shiro的配置文件中需要注意的地方:
<!-- Realm 实现 -->
<bean id="userRealm" class="com.seawave.shiro.realm.UserRealm">
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="${authenticationCacheName}"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="${authorizationCacheName}"/>
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="cachingEnabled" value="true"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="supportedLoginType" value="usernameAndPassword"/>
</bean>
<bean id="verifyCodeRealm" class="com.seawave.shiro.realm.VerifyCodeRealm">
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="${userAndCaptchaCacheName}"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="${authorizationCacheName}"/>
<!--这里使用shiro自带的一个凭证验证器,根据字节码判断是否一致-->
<property name="credentialsMatcher" ref="simpleCredentialsMatcher"/>
<property name="cachingEnabled" value="true"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="supportedLoginType" value="usernameAndCaptcha"/>
</bean>
<!--凭证匹配器-->
<bean id="authenticator" class="com.seawave.shiro.authc.pam.MyModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="verifyCodeRealm"/>
<ref bean="userRealm"/>
</list>
</property>
</bean>
<!--权限匹配器-->
<bean id="authorizer" class="org.apache.shiro.authz.ModularRealmAuthorizer">
<property name="realms">
<list>
<ref bean="verifyCodeRealm"/>
<ref bean="userRealm"/>
</list>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="authorizer" ref="authorizer"/>
</bean>
<!-- 基于Form表单的身份验证过滤器 -->
<!-- shiro的身份验证成功后的逻辑是这样的:如果原来的请求里面存在了一个请求地址,如:http://localhost/a/b/c,那么此时-->
<!-- shiro将自动跳转到a/b/c页面或对应的Controller,这时候自定义的successUrl是不生效的-->
<!-- 如果想要跳转到指定的页面只能重写onLoginSuccess()方法:-->
<!-- 1.进行重定向-->
<!-- 2.清空请求里面的请求地址,此时因为请求地址为空,则会使用我们自定义的successUrl-->
<bean id="formAuthenticationFilter"
class="com.seawave.shiro.captcha.MyFormAuthenticationFilter">
<property name="usernameParam" value="name"/>
<property name="passwordParam" value="pwd"/>
<!--该参数定义了用户表单中提交的'登陆类型'这个参数的名称-->
<property name="loginTypeParamName" value="loginType"/>
<property name="rememberMeParam" value="rememberMe"/>
<property name="successUrl" value="/homePage/gotoHomePage"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="kickOutSessionCacheName" value="${kickOutSessionCacheName}"/>
<property name="kickOutAfter" value="false"/>
<property name="maxSession" value="1"/>
</bean>
总结
多看源码
耐心细致