spring boot 2.x + shiro实现同一用户多种登录方式

继上篇关于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 menuList = menuService.getAllMenuByRoleId(role.getId());
                        if(CollectionUtils.isNotEmpty(menuList)){
                            for (Menu menu : menuList){
                                if(StringUtils.isNoneBlank(menu.getPermission())){
                                    info.addStringPermission(menu.getPermission());
                                }
                            }
                        }
                    }
                }
            }
        }
        log.info("---------------- 获取到以下权限 ----------------");
        log.info(info.getStringPermissions().toString());
        log.info("---------------- Shiro 权限获取成功 ----------------------");
        return info;
    }

    /**
     * 认证信息.(身份验证) : Authentication 是用来验证用户身份
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException{
        return null;
    }
}

第五步:创建自定义的多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.密码登录

spring boot 2.x + shiro实现同一用户多种登录方式_第1张图片

spring boot 2.x + shiro实现同一用户多种登录方式_第2张图片

2.手机验证码登录

spring boot 2.x + shiro实现同一用户多种登录方式_第3张图片

spring boot 2.x + shiro实现同一用户多种登录方式_第4张图片

3.微信登录

spring boot 2.x + shiro实现同一用户多种登录方式_第5张图片

spring boot 2.x + shiro实现同一用户多种登录方式_第6张图片

4.调用需要授权访问的方法

spring boot 2.x + shiro实现同一用户多种登录方式_第7张图片

spring boot 2.x + shiro实现同一用户多种登录方式_第8张图片

由此可见,我们已经实现了同一用户的不同登录方式的功能,但是需要注意的是本文只是介绍了同一用户同一角色的情况,如果是不同用户不同角色的登录,还请参考其他博文,感谢!

你可能感兴趣的:(shiro)