继上篇关于shiro使用的博文介绍,我们已经初步了解了在spring boot中如何使用shiro,但是当我们集成shiro实现登录认证的时候会发现,我们的系统支持多种登录方式(密码登录、手机验证码登录、其他第三方登录),而且每种登录方式的具体实现又会存在差异,这时候我们就需要用shiro的多realm的方式去实现登录,每个realm对应不同的登录方式的具体实现。
此处举例以密码登录、手机验证码登录、微信登录三种登录方式为例;以下只贴上篇博文修改过的代码,其他代码请查看spring boot 2.0.0 + shiro + redis实现前后端分离的项目,或者请直接查看源码
第一步:首先我们要建立一个登录方式的枚举类,存放我们所有的登录方式
public enum LoginType {
/**
* 通用
*/
COMMON("common_realm"),
/**
* 用户密码登录
*/
USER_PASSWORD("user_password_realm"),
/**
* 手机验证码登录
*/
USER_PHONE("user_phone_realm"),
/**
* 第三方登录(微信登录)
*/
WECHAT_LOGIN("wechat_login_realm");
private String type;
private LoginType(String type) {
this.type = type;
}
public String getType() {
return type;
}
@Override
public String toString() {
return this.type.toString();
}
}
第二步:我们需要建立自定义的登录身份UserToken,需要继承org.apache.shiro.authc.UsernamePasswordToken
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* 自定义登录身份
*/
public class UserToken extends UsernamePasswordToken {
//登录方式
private LoginType loginType;
//微信code
private String code;
// TODO 由于是demo方法,此处微信只传一个code参数,其他参数根据实际情况添加
public UserToken(LoginType loginType, final String username, final String password) {
super(username, password);
this.loginType = loginType;
}
public UserToken(LoginType loginType, String username, String password, String code) {
super(username, password);
this.loginType = loginType;
this.code = code;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
第三步:建立各自登录方式对应的realm(UserPasswordRealm、UserPhoneRealm、WechatLoginRealm),并实现具体的登录方法
3.1 用户密码登录realm
/**
* 用户密码登录realm
*/
@Slf4j
public class UserPasswordRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
public String getName() {
return LoginType.USER_PASSWORD.getType();
}
@Override
public boolean supports(AuthenticationToken token) {
if (token instanceof UserToken) {
return ((UserToken) token).getLoginType() == LoginType.USER_PASSWORD;
} else {
return false;
}
}
@Override
public void setAuthorizationCacheName(String authorizationCacheName) {
super.setAuthorizationCacheName(authorizationCacheName);
}
@Override
protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
log.info("---------------- 用户密码登录 ----------------------");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String name = token.getUsername();
// 从数据库获取对应用户名密码的用户
User user = userService.getUserByName(name);
if (user != null) {
// 用户为禁用状态
if (!user.getLoginFlag().equals("1")) {
throw new DisabledAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户
user.getPassword(), //密码
getName() //realm name
);
return authenticationInfo;
}
throw new UnknownAccountException();
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
3.2 用户手机登录realm
/**
* 手机验证码登录realm
*/
@Slf4j
public class UserPhoneRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
public String getName() {
return LoginType.USER_PHONE.getType();
}
@Override
public boolean supports(AuthenticationToken token) {
if (token instanceof UserToken) {
return ((UserToken) token).getLoginType() == LoginType.USER_PHONE;
} else {
return false;
}
}
@Override
public void setAuthorizationCacheName(String authorizationCacheName) {
super.setAuthorizationCacheName(authorizationCacheName);
}
@Override
protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
log.info("---------------- 手机验证码登录 ----------------------");
UserToken token = (UserToken) authcToken;
String phone = token.getUsername();
// 手机验证码
String validCode = String.valueOf(token.getPassword());
// 这里假装从redis中获取了验证码为 123456,并对比密码是否正确
if(!"123456".equals(validCode)){
log.debug("验证码错误,手机号为:{}", phone);
throw new IncorrectCredentialsException();
}
User user = userService.getByPhone(phone);
if(user == null){
throw new UnknownAccountException();
}
// 用户为禁用状态
if(user.getLoginFlag().equals("0")){
throw new DisabledAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户
validCode, //密码
getName() //realm name
);
return authenticationInfo;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
3.3 微信登录realm
/**
* 微信登录realm
*/
@Slf4j
public class WechatLoginRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
public String getName() {
return LoginType.WECHAT_LOGIN.getType();
}
@Override
public boolean supports(AuthenticationToken token) {
if (token instanceof UserToken) {
return ((UserToken) token).getLoginType() == LoginType.WECHAT_LOGIN;
} else {
return false;
}
}
@Override
public void setAuthorizationCacheName(String authorizationCacheName) {
super.setAuthorizationCacheName(authorizationCacheName);
}
@Override
protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
log.info("---------------- 微信登录 ----------------------");
UserToken token = (UserToken) authcToken;
String code = token.getCode();
String openid = getOpenid(code);
if(StringUtils.isEmpty(openid)){
log.debug("微信授权登录失败,未获得openid");
throw new AuthenticationException();
}
User user = userService.getByOpenid(openid);
if(user == null){
// TODO 获取微信昵称、头像等信息,并完成注册用户,此处省略
}
// 用户为禁用状态
if(user.getLoginFlag().equals("0")){
throw new DisabledAccountException();
}
// 完成登录,此处已经不需要做密码校验
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户
code, //密码
getName() //realm name
);
return authenticationInfo;
}
private String getOpenid(String code){
// 这里假装是一个通过code获取openid的方法,具体实现由各位自己去实现,此处不做扩展
if(StringUtils.isNotEmpty(code)){
return "sdfuh81238917jhoijiosdsgsdfljiofds";
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
第四步: 建立统一角色授权管理realm,并实现授权方法;查看第三步所创建的realm,你会发现所有的doGetAuthoriztionInfo方法都是返回的null,也就是不做授权处理,我们会在AuthorizationRealm中做统一的授权操作;
这样处理的原因是,当我们往shiro的SecurityManager中注入多个realm后,当一个新用户调用需授权的方法时,会依次调用所有的realm的doGetAuthoriztionInfo方法,而不是根据登录类型来调用不同realm的授权方法;因此为了避免重复代码(或者说重复授权),我们这边做统一处理。
ps:本人这边是用这种方法处理的,如果各位大神有更好的方法,还请不吝指教,万分感谢
/**
* 统一角色授权控制策略
*/
@Slf4j
public class AuthorizationRealm extends AuthorizingRealm {
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("---------------- 执行 Shiro 权限获取 ---------------------");
Object principal = principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principal instanceof User) {
User userLogin = (User) principal;
if(userLogin != null){
List roleList = roleService.findByUserid(userLogin.getId());
if(CollectionUtils.isNotEmpty(roleList)){
for(Role role : roleList){
info.addRole(role.getEnname());
List
第五步:创建自定义的多relam的管理策略,继承org.apache.shiro.authc.pam.ModularRealmAuthenticator并实现doAuthenticate方法
/**
* 自定义多realm登录策略
*/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 所有Realm
Collection realms = getRealms();
// 登录类型对应的所有Realm
HashMap realmHashMap = new HashMap<>(realms.size());
for (Realm realm : realms) {
realmHashMap.put(realm.getName(), realm);
}
UserToken token = (UserToken) authenticationToken;
// 登录类型
LoginType loginType = token.getLoginType();
if (realmHashMap.get(loginType.getType()) != null) {
return doSingleRealmAuthentication(realmHashMap.get(loginType.getType()), token);
} else {
return doMultiRealmAuthentication(realms, token);
}
}
}
第六步:shiro的配置类修改,原本为单realm,现改为多realm
@Configuration
public class ShiroConfig {
/**
* 此处只贴出修改的代码,其他代码请详见上篇博文
*/
/**
* SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setAuthenticator(modularRealmAuthenticator());
List realms = new ArrayList<>();
// 统一角色权限控制realm
realms.add(authorizingRealm());
// 用户密码登录realm
realms.add(userPasswordRealm());
// 用户手机号验证码登录realm
realms.add(userPhoneRealm());
// 微信登录realm
realms.add(wechatLoginRealm());
securityManager.setRealms(realms);
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 自定义的Realm管理,主要针对多realm
*/
@Bean("myModularRealmAuthenticator")
public MyModularRealmAuthenticator modularRealmAuthenticator() {
MyModularRealmAuthenticator customizedModularRealmAuthenticator = new MyModularRealmAuthenticator();
// 设置realm判断条件
customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return customizedModularRealmAuthenticator;
}
@Bean
public AuthorizingRealm authorizingRealm(){
AuthorizationRealm authorizationRealm = new AuthorizationRealm();
authorizationRealm.setName(LoginType.COMMON.getType());
return authorizationRealm;
}
/**
* 密码登录realm
*
* @return
*/
@Bean
public UserPasswordRealm userPasswordRealm() {
UserPasswordRealm userPasswordRealm = new UserPasswordRealm();
userPasswordRealm.setName(LoginType.USER_PASSWORD.getType());
// 自定义的密码校验器
userPasswordRealm.setCredentialsMatcher(credentialsMatcher());
return userPasswordRealm;
}
/**
* 手机号验证码登录realm
* @return
*/
@Bean
public UserPhoneRealm userPhoneRealm(){
UserPhoneRealm userPhoneRealm = new UserPhoneRealm();
userPhoneRealm.setName(LoginType.USER_PHONE.getType());
return userPhoneRealm;
}
/**
* 微信授权登录realm
* @return
*/
@Bean
public WechatLoginRealm wechatLoginRealm(){
WechatLoginRealm wechatLoginRealm = new WechatLoginRealm();
wechatLoginRealm.setName(LoginType.WECHAT_LOGIN.getType());
return wechatLoginRealm;
}
@Bean
public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher();
}
}
第七步:修改并添加登录方法,实现密码登录、手机验证码登录、微信登录的入口
@RestController
@RequestMapping("/admin")
public class LoginController {
/**
* 用户密码登录
* @param loginName
* @param pwd
* @return
*/
@RequestMapping("/login")
public Result login(String loginName, String pwd){
UserToken token = new UserToken(LoginType.USER_PASSWORD, loginName, pwd);
return shiroLogin(token);
}
/**
* 手机验证码登录
* 注:由于是demo演示,此处不添加发送验证码方法;
* 正常操作:发送验证码至手机并且将验证码存放在redis中,登录的时候比较用户穿过来的验证码和redis中存放的验证码
* @param phone
* @param code
* @return
*/
@RequestMapping("phoneLogin")
public Result phoneLogin(String phone, String code){
// 此处phone替换了username,code替换了password
UserToken token = new UserToken(LoginType.USER_PHONE, phone, code);
return shiroLogin(token);
}
/**
* 微信登录
* 注:由于是demo演示,此处只接收一个code参数(微信会生成一个code,然后通过code获取openid等信息)
* 其他根据个人实际情况添加参数
* @param code
* @return
*/
@RequestMapping("wechatLogin")
public Result wechatLogin(String code){
// 此处假装code分别是username、password
UserToken token = new UserToken(LoginType.WECHAT_LOGIN, code, code, code);
return shiroLogin(token);
}
public Result shiroLogin(UserToken token){
try {
//登录不在该处处理,交由shiro处理
Subject subject = SecurityUtils.getSubject();
subject.login(token);
if (subject.isAuthenticated()) {
JSON json = new JSONObject();
((JSONObject) json).put("token", subject.getSession().getId());
return new Result(ResultStatusCode.OK, json);
}else{
return new Result(ResultStatusCode.SHIRO_ERROR);
}
}catch (IncorrectCredentialsException | UnknownAccountException e){
e.printStackTrace();
return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD);
}catch (LockedAccountException e){
return new Result(ResultStatusCode.USER_FROZEN);
}catch (Exception e){
return new Result(ResultStatusCode.SYSTEM_ERR);
}
}
/**
* 退出登录
* @return
*/
@RequestMapping("/logout")
public Result logout(){
SecurityUtils.getSubject().logout();
return new Result(ResultStatusCode.OK);
}
}
至此我们就已经完成了同一用户多种登录方式的实现,下面我们测试一下
1.密码登录
2.手机验证码登录
3.微信登录
4.调用需要授权访问的方法
由此可见,我们已经实现了同一用户的不同登录方式的功能,但是需要注意的是本文只是介绍了同一用户同一角色的情况,如果是不同用户不同角色的登录,还请参考其他博文,感谢!