前言
大部分场景下,我们都会在项目中实现自定义 Realm
搭配 UsernamePasswordToken
来完成用户的登录认证流程,但是如果登录方式包括“第三方登录”、“手机号登录”等,仅凭 UsernamePasswordToken
就难以实现了,因为以上的两种登录方式都是免密登录,而 UsernamePasswordToken
却必须要有 username
和 password
,因此需要自定义多个 Realm 和 Token 才能实现上述功能。
本文只会实现第三方登录,以此为例,列位看官可以尝试修改代码实现自己的业务逻辑。
另外,本文篇幅较长,代码量较多,看官们一定要耐心看完,万一漏写或写错了部分代码,耽误的可是更多的时间。
创建自定义 Token
网上很多文章都继承 UsernamePasswordToken
来创建自己的 Token,但我不建议这样写,如果继承 UsernamePasswordToken
,在后面的操作中会变得相对麻烦。
我们直接查看 UsernamePasswordToken
的源码,可以看到它实现了 HostAuthenticationToken
和 RememberMeAuthenticationToken
,而这两个类又分别实现了 AuthenticationToken
,因此在这里我们直接实现 AuthenticationToken
即可,同时重写 getPrincipal()
和 getCredentials()
两个方法。
import org.apache.shiro.authc.AuthenticationToken; /** * 第三方授权登录凭证 * 注意这里要实现AuthenticationToken,不能继承UsernamePasswordToken * 同时重写getPrincipal()和getCredentials()两个方法 * @author wujiawei0926@yeah.net */ public class OAuth2UserToken implements AuthenticationToken { /** * 授权类型 * 这里可以使用枚举 */ private String type; // 第三方登录后获取的用户信息 private OAuth2User user; public OAuth2UserToken(final String type, final OAuth2User user) { this.type = type; this.user = user; } @Override public Object getPrincipal() { return this.getUser(); } @Override public Object getCredentials() { return this.getUser().getOpenid(); } public String getType() { return type; } public void setType(String type) { this.type = type; } public OAuth2User getUser() { return user; } public void setUser(OAuth2User user) { this.user = user; } /** * 用户信息类,用于新用户注册 * 可根据自己的具体业务进行拓展 */ public static class OAuth2User { public OAuth2User(){}; private String openid; private String username; private String nickname; private String avatar; private String email; private String remark; private Integer sex; public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } } }
创建多个 Realm
创建 Realm 时,必须重写 supports()
方法,在后面起到了至关重要的作用。
作为演示,本文创建的 Realm 都没有做权限的授权,即 doGetAuthorizationInfo()
没有做具体的实现,列位看官需要加上自己的权限授权业务。
首先是传统的 UserRealm
,相信列位看官对这个类都很熟悉,因此这里不再赘述,直接贴上代码:
import com.*.dao.UserDao; import com.*.model.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; 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 org.springframework.beans.factory.annotation.Autowired; /** * Shiro认证和授权 * @author wujiawei0926@yeah.net */ public class UserRealm extends AuthorizingRealm { @Autowired private UserDao userService; /** * 一定要重写support()方法,在后面的身份验证器中会用到 * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user = (User) SecurityUtils.getSubject().getPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); return authorizationInfo; } /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username = (String) authenticationToken.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException(); // 账号不存在 } if (user.getDisabled() == 1) { throw new LockedAccountException(); } if (user.getUserType() != 0) { throw new ConcurrentAccessException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return authenticationInfo; } }
然后创建我们的免密登陆 Realm,与 UserRealm
相同,继承 AuthorizingRealm
即可,同样的,也要重写 supports()
方法,并且再多重写一个 getName()
方法,在后面也会用到。
用户登录的方法写在 doGetAuthenticationInfo
中,通过校验 openid
,实现老用户的登录和新用户的注册,下面贴上代码:
import com.*.dao.UserDao; import com.w.module.base.model.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 自定义第三方登录授权Realm * @author wujiawei0926@yeah.net */ @Component public class OAuth2UserRealm extends AuthorizingRealm { public static final String REALM_NAME = "oauth2_user_realm"; @Autowired private UserDao userDao; @Override public String getName() { return REALM_NAME; } /** * 检查是否支持该Realm * 一定要重写support()方法,在后面的身份验证器中会用到 * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof OAuth2UserToken; } /** * 授权 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user = (User) SecurityUtils.getSubject().getPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); return authorizationInfo; } /** * 认证 * 在这个方法中,完成老用户的登录与新用户的注册 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { OAuth2UserToken token = (OAuth2UserToken)authenticationToken; OAuth2UserToken.OAuth2User oAuth2User = token.getUser(); // 校验openid if (oAuth2User == null) { throw new AuthenticationException(); } // 根据openid查询用户数据 String openid = oAuth2User.getOpenid(); User user = null; switch (token.getType()) { case "qq": user = userDao.findByQqOpenid(openid); break; case "weixin": user = userDao.findByWxOpenid(openid); break; default: break; } if (user == null) { // TODO 获取oAuth2User中用户信息进行注册 } if (user.getDisabled() == 1) { // 账号被拉黑 throw new LockedAccountException(); } // 完成登录,注意这里传的principal和credentials // principal: OAuth2UserToken类中getPrincipal()的返回值 // credentials: OAuth2UserToken类中getCredentials()的返回值 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, openid, getName()); return authenticationInfo; } }
创建自定义 ModularRealmAuthenticator
Token
和 Realm
创建完成之后,需要再创建一个 ModularRealmAuthenticator
来进行绑定操作,让程序知道碰到这个 Token
时进入对应的 Realm
。
这步其实很简单,只需要重写 doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
方法,该方法有两个参数,其中 realms
是 ShiroConfig
中配置的所有 realm 集合,token
就是登录时传入的用户信息 token,在我们这边只会是 UsernamePasswordToken
或 OAuth2UserToken
。
在 doMultiRealmAuthentication()
方法中遍历所有的 realm,通过每个 realm 的 supports()
方法来进行匹配。
package com.w.common.config.shiro;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.realm.Realm;
import java.util.Collection;
/**
* 自定义身份验证器,根据登录使用的Token匹配调用对应的Realm
* @author wujiawei0926@yeah.net
*/
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
/**
* 自定义Realm的分配策略
* 通过realm.supports()方法匹配对应的Realm,因此才要在Realm中重写supports()方法
* @param realms
* @param token
* @return
*/
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 通过supports()方法,匹配对应的Realm
Realm uniqueRealm = null;
for (Realm realm : realms) {
if (realm.supports(token)) {
uniqueRealm = realm;
break;
}
}
if (uniqueRealm == null) {
throw new UnsupportedTokenException();
}
return uniqueRealm.getAuthenticationInfo(token);
}
}
配置 ShiroConfig
ShiroConfig
中需要添加或修改一下配置:
modularRealmAuthenticator
,告诉 Shiro,以后使用我们自定义的身份验证器:
/** * 针对多Realm,使用自定义身份验证器 * @return */ @Bean public ModularRealmAuthenticator modularRealmAuthenticator(){ CustomModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator(); authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return authenticator; }
realm
,把创建的所有 realm 都注册到 bean 中:
/** * 免密授权登录 * @return */ @Bean public OAuth2UserRealm oAuth2UserRealm(){ OAuth2UserRealm realm = new OAuth2UserRealm(); // 不需要加密,直接返回 return realm; } @Bean public UserRealm userRealm() { UserRealm userRealm = new UserRealm(); userRealm.setCredentialsMatcher(credentialsMatcher()); return userRealm; }
securityManager
,让安全管理器使用我们创建的身份验证器,并添加所有的 realm:
@Bean(name = "securityManager") public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setCacheManager(cacheManager()); securityManager.setSessionManager(sessionManager()); // 设置验证器为自定义验证器 securityManager.setAuthenticator(modularRealmAuthenticator()); // 设置Realms List<Realm> realms = new ArrayList<>(2); realms.add(userRealm()); realms.add(oAuth2UserRealm()); securityManager.setRealms(realms); return securityManager; }
最后把我的 ShiroConfig
完整代码奉上,各位参考即可:
import com.*.config.filter.MyLoginFilter; import com.*.config.filter.MyLogoutFilter; import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.eis.MemorySessionDAO; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.web.filter.DelegatingFilterProxy; import javax.servlet.Filter; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * shiro框架配置 */ @Configuration public class ShiroConfig { @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); // 登录配置 shiroFilter.setLoginUrl("login"); shiroFilter.setSuccessUrl("system"); shiroFilter.setUnauthorizedUrl("/error?code=403"); // 自定义过滤器 Map<String, Filter> filtersMap = new LinkedHashMap<>(); filtersMap.put("access", new MyLoginFilter()); filtersMap.put("mylogout", new MyLogoutFilter()); shiroFilter.setFilters(filtersMap); // 拦截配置 Map<String, String> filterChainDefinitions = new LinkedHashMap<>(); filterChainDefinitions.put("/logout", "mylogout"); filterChainDefinitions.put("/system/**", "access,authc"); filterChainDefinitions.put("/**", "anon"); shiroFilter.setFilterChainDefinitionMap(filterChainDefinitions); return shiroFilter; } @Bean(name = "securityManager") public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setCacheManager(cacheManager()); securityManager.setSessionManager(sessionManager()); // 设置验证器为自定义验证器 securityManager.setAuthenticator(modularRealmAuthenticator()); // 设置Realms List<Realm> realms = new ArrayList<>(2); realms.add(userRealm()); realms.add(oAuth2UserRealm()); securityManager.setRealms(realms); return securityManager; } /** * 针对多Realm,使用自定义身份验证器 * @return */ @Bean public ModularRealmAuthenticator modularRealmAuthenticator(){ CustomModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator(); authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return authenticator; } /** * 免密授权登录 * @return */ @Bean public OAuth2UserRealm oAuth2UserRealm(){ OAuth2UserRealm realm = new OAuth2UserRealm(); // 不需要加密,直接返回 return realm; } @Bean @DependsOn("lifecycleBeanPostProcessor") public UserRealm userRealm() { UserRealm userRealm = new UserRealm(); userRealm.setCredentialsMatcher(credentialsMatcher()); return userRealm; } @Bean public DefaultWebSessionManager sessionManager(){ DefaultWebSessionManager manager = new DefaultWebSessionManager(); manager.setSessionDAO(sessionDAO()); manager.setGlobalSessionTimeout(10800000); manager.setDeleteInvalidSessions(true); manager.setSessionValidationSchedulerEnabled(true); manager.setSessionValidationInterval(10800000); return manager; } @Bean public SessionDAO sessionDAO(){ return new MemorySessionDAO(); } @Bean(name = "cacheManager") public EhCacheManager cacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:shiro/ehcache-shiro.xml"); return cacheManager; } @Bean(name = "credentialsMatcher") public CredentialsMatcher credentialsMatcher() { return new CredentialsMatcher(); } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor(); return lifecycleBeanPostProcessor; } /** * shiro里实现的Advisor类,用来拦截注解的方法 . */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager()); return advisor; } @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public FilterRegistrationBean delegatingFilterProxy(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName("shiroFilter"); filterRegistrationBean.setFilter(proxy); return filterRegistrationBean; } }
登录
上面都配置好之后,终于到了登录。
这时候就很简单了,在登录方法中,实例化对应的 Token,然后调用 subject 的 login()
方法,及时捕获异常,没有排除异常代表登录认证成功。
例如传统的账号密码登录,这里的 username
和 password
就是前端传的值:
try { UsernamePasswordToken token = new UsernamePasswordToken(username, password); SecurityUtils.getSubject().login(token); return Result.ok("登录成功"); } catch (IncorrectCredentialsException ice) { return Result.error("密码错误"); } catch (UnknownAccountException uae) { return Result.error("账号不存在"); } catch (LockedAccountException e) { return Result.error("账号被锁定"); } catch (ExcessiveAttemptsException eae) { return Result.error("操作频繁,请稍后再试"); } catch (ConcurrentAccessException cae) { return Result.error("没有权限,无法登陆"); }
而第三方登录就实例化 OAuth2UserToken
,将第三方平台的类型及其回调的 openid 等信息保存到 token 中,然后调用 subject 的 login()
方法:
// 实例化自定义的授权Token OAuth2UserToken.OAuth2User oAuth2User = new OAuth2UserToken.OAuth2User(); oAuth2User.setOpenid(authUser.getUuid()); oAuth2User.setNickname(authUser.getNickname()); OAuth2UserToken userToken = new OAuth2UserToken(type, oAuth2User); // 调用login方法 SecurityUtils.getSubject().login(userToken); servletResponse.sendRedirect("/");
结束
将上面代码全部写好之后,启动项目,尝试用户名密码登录和第三方免密登录,如果没有问题则大功告成,如果报错了,看官应注意检查是否遗漏代码。
同时注意,上面的代码切忌照搬,还是以理解为主。
如果存在疑问,或者有任何的建议,欢迎评论留言!
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于