50、自定义token兼容手机、邮箱等多身份信息登录

自定义token兼容手机、邮箱等多身份信息登录

一、前言

在登录认证中,用户名密码登录到系统是必然的模式,但是很多系统或日常中,可以有很多其他身份登陆到对应系统中,比如手机号码、邮箱、微信等,在oauth2中要实现对应登录方式,必须要实现对应的Filter、Provider、对应token以及UserDetailService接口,使得不同的方式创建不同的token,然后由与之对应的Provider进行认证,最后Provider使用UserDetail接口通过不同的参数载入相同的用户信息,最后生成对应token信息返回到前端。熟悉oauth2的人就很容易明白在shiro其实也是一样的道理。下面就直接进入正题,带大家如何实现该需求。

二、实现

shiro的登录流程是这样的,首先登录登录的过程中shiro将浏览器request请求传入的session创建一个与之对应的Subject,然后由用户创建UsernamePasswordToken并传入到Subject的login方法中实现登录。登录方法具体流程是Subject将token传给SecurityManager,然后SecurityManager调用注入到其内部的自定义Realm,由Realm从token中获取信息并从数据库(或redis或其他缓存)中提取用户认证信息给shiro,shiro通过返回的认证信息与实际的密码做比对如果相同则通过并保存session到缓存(如果有),如果认证过程失败抛出异常。

连接用户名密码登录的流程后,在shiro中使用的令牌是UsernamePasswordToken,我们可以参考UsernamePasswordToken,自定义PhoneToken,在不同的控制器中传入Token,然后由Realm判断当前的Token属于UsernamePasswordToken还是PhoneToken,从而实现用户多身份验证。

(1)自定义token(以手机号码为例子)

package com.dondown.token;
import java.io.Serializable;
import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;
public class CellphoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable{
     private static final long serialVersionUID = 1L;
     
     // 移动电话
     private String phone;
     // 登录密码
     private String password;
     // 记住我
     private boolean rememberMe;
     // 主机
     private String host;
     
     public CellphoneToken(){
          this.rememberMe = false;
     }
     
     public CellphoneToken(String phone, String password){
          this.phone = phone;
          this.password = password;
          this.rememberMe = false;
     }
     
     public CellphoneToken(String phone, String password, boolean rememberMe){
          this.phone = phone;
          this.password = password;
          this.rememberMe = rememberMe;
     }
     
     /**
      * 获取身份信息
      */
     @Override
     public Object getPrincipal() {
          return phone;
     }
     /**
      * 获取凭证
      */
     @Override
     public Object getCredentials() {
          return password;
     }
     @Override
     public boolean isRememberMe() {
          return rememberMe;
     }
     @Override
     public String getHost() {
          return host;
     }
}

(2)添加自定义手机的realm

package com.dondown.realm;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
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;
importorg.springframework.stereotype.Component;
import com.dondown.model.Right;
import com.dondown.model.Role;
import com.dondown.model.User;
import com.dondown.service.RightService;
import com.dondown.service.RoleService;
import com.dondown.service.UserService;
import com.dondown.token.CellphoneToken;
@Component
public class ShiroCellphoneRealm extends AuthorizingRealm{
     @Autowired
     private UserService userService;
     @Autowired
     private RoleService roleService;
     @Autowired
     private RightService rightService;
     
     /**
      * Subject.login(token)的时候就会调用doGetAuthenticationInfo方法
      */
     @Override
     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          /**
           * 这里为什么是String类型呢?其实要根据Subject.login(token)时候的token来的,你token定义成的pricipal是啥,这里get的时候就是啥。比如我
              Subject subject = SecurityUtils.getSubject();
              UsernamePasswordToken idEmail = new UsernamePasswordToken(String.valueOf(user.getId()), user.getEmail());
              try {
                   idEmail.setRememberMe(true);
                   subject.login(idEmail);
              }
               AuthenticationToken 为自己login传入的token子类
          **/
          
          // 手机号码登录
          assert(token instanceof CellphoneToken);
          String userName = (String) token.getPrincipal();
          User user = userService.findByCellphone(userName);;
          
          if (user == null) {
              throw new UnknownAccountException("该手机号不存在");
          }
          if(user.getEnabled() != 1){
              throw new DisabledAccountException("用户未启用");
          }
          if(user.getLocked() != 0){
              throw new LockedAccountException("用户被锁定");
          }
          
          // 主体,一般存用户名或用户实例对象,用于在其他地方获取当前认证用户信息
        Object principal = user;
          
          // SimpleAuthenticationInfo还有其他构造方法,比如密码加密算法等,感兴趣可以自己看
          SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                        principal,                        // 表示凭证
                        user.getPassword(),             // 表示散列后的密钥如密码
                        ByteSourceUtils.bytes(userName),// 以用户名作为散列盐值,如果配置了自己的matcher,那么需要自定义matcher实现认证方法
                        getName()                         // 领域名称(数据源名)
          );
          // authenticationInfo信息交个shiro,调用login的时候会自动比较这里的token和authenticationInfo
          return authenticationInfo;
     }
     
     /**
      * 该realm支持什么token验证
      */
     @Override
    public boolean supports(AuthenticationToken token){
        return token instanceof CellphoneToken;
    }
     
     /**
      * PrincipalCollection : 用户身份信息,包括用户名、手机、邮箱等信息,primary表示主要的分身信息
      * 获取用户权限信息,当涉及到Subject.hasRole或者Subject.hasPermission
      * 的时候就会调用doGetAuthorizationInfo方法;
      */
     @Override
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
          SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
          
          //获取用户
        //User user = (User) SecurityUtils.getSubject().getPrincipal();
       
          // 根据用户主要身份信息获取用户角色信息
          String userName = (String) principals.getPrimaryPrincipal();
          List<Role> roles = roleService.findByUserName(userName);
          Set<String> roleNames = new HashSet<>(roles.size());
          for (Role role : roles) {
              roleNames.add(role.getName());
          }
          // 此处把当前subject对应的所有角色信息交给shiro
          // 调用hasRole的时候就根据这些role信息判断
          authorizationInfo.setRoles(roleNames);
          // 根据用户主要信息获取用户权限信息
          List<Right> permissions = rightService.findByUsername(userName);
          Set<String> permissionNames = new HashSet<>(permissions.size());
          for (Right permission : permissions) {
              permissionNames.add(permission.getName());
          }
          // 此处把当前subject对应的权限信息交给shiro
          // 当调用hasPermission的时候就会根据这些信息判断
          authorizationInfo.setStringPermissions(permissionNames);
          return authorizationInfo;
     }
     
     /**
     * 重写方法,清除当前用户的的 授权缓存
     * @param principals
     */
    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
    /**
     * 重写方法,清除当前用户的 认证缓存
     * @param principals
     */
    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }
   
    /**
     * 自定义方法:清除所有 授权缓存
     */
    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }
    /**
     * 自定义方法:清除所有 认证缓存
     */
    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }
    /**
     * 自定义方法:清除所有的  认证缓存  和 授权缓存
     */
    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }
}

注意:
在有多个自定义token情况下,重写support方法,在login的时候会遍历所有realm,如果对应realm支持login传入的token则会被调用,直到命中。所以我们在对应的realm中指定支持的token即可。

另外,之前兼容用户名密码形式登录的token我们这里也有,代码如下:

package com.dondown.realm;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
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;

import com.dondown.model.Right;
import com.dondown.model.Role;
import com.dondown.model.User;
import com.dondown.service.RightService;
import com.dondown.service.RoleService;
import com.dondown.service.UserService;

@Component
public class ShiroUserRealm extends AuthorizingRealm{
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private RightService rightService;
    
    /**
     * Subject.login(token)的时候就会调用doGetAuthenticationInfo方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        /**
         * 这里为什么是String类型呢?其实要根据Subject.login(token)时候的token来的,你token定义成的pricipal是啥,这里get的时候就是啥。比如我
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken idEmail = new UsernamePasswordToken(String.valueOf(user.getId()), user.getEmail());
            try {
                idEmail.setRememberMe(true);
                subject.login(idEmail);
            }
              AuthenticationToken 为自己login传入的token子类
        **/
        String userName = (String) token.getPrincipal();
        User user = userService.findByUsername(userName);
        if (user == null) {
            throw new UnknownAccountException("用户不存在");
        }
        if(user.getEnabled() != 1){
            throw new DisabledAccountException("用户未启用");
        }
        if(user.getLocked() != 0){
            throw new LockedAccountException("用户被锁定");
        }
        
        // 主体,一般存用户名或用户实例对象,用于在其他地方获取当前认证用户信息
        Object principal = user;
        
        // SimpleAuthenticationInfo还有其他构造方法,比如密码加密算法等,感兴趣可以自己看
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    principal,                           // 表示凭证,可以随便设,但是其他地方可能会获取使用
                    user.getPassword(),                 // 表示散列后的密钥如密码
                    ByteSourceUtils.bytes(userName),    // 用用户名作为散列盐值,如果配置了自己的matcher,那么需要自定义matcher实现认证方法
                    getName()                            // 领域名称(数据源名)
        );

        // authenticationInfo信息交个shiro,调用login的时候会自动比较这里的token和authenticationInfo
        return authenticationInfo;
    }
    
    /**
     * PrincipalCollection : 用户身份信息,包括用户名、手机、邮箱等信息,primary表示主要的分身信息
     * 获取用户权限信息,当涉及到Subject.hasRole或者Subject.hasPermission
     * 的时候就会调用doGetAuthorizationInfo方法;
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        
        // 获取用户
        //User user = (User) SecurityUtils.getSubject().getPrincipal();
        
        // 根据用户主要身份信息获取用户角色信息
        String userName = (String) principals.getPrimaryPrincipal();
        List<Role> roles = roleService.findByUserName(userName);
        Set<String> roleNames = new HashSet<>(roles.size());
        for (Role role : roles) {
            roleNames.add(role.getName());
        }
        // 此处把当前subject对应的所有角色信息交给shiro
        // 调用hasRole的时候就根据这些role信息判断
        authorizationInfo.setRoles(roleNames);

        // 根据用户主要信息获取用户权限信息
        List<Right> permissions = rightService.findByUsername(userName);
        Set<String> permissionNames = new HashSet<>(permissions.size());
        for (Right permission : permissions) {
            permissionNames.add(permission.getName());
        }
        // 此处把当前subject对应的权限信息交给shiro
        // 当调用hasPermission的时候就会根据这些信息判断
        authorizationInfo.setStringPermissions(permissionNames);

        return authorizationInfo;
    }

    /**
     * 该realm支持什么token验证
     */
    @Override
    public boolean supports(AuthenticationToken token){
        return token instanceof UsernamePasswordToken;
    }
    
    /**
     * 重写方法,清除当前用户的的 授权缓存
     * @param principals
     */
    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    /**
     * 重写方法,清除当前用户的 认证缓存
     * @param principals
     */
    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }
    
    /**
     * 自定义方法:清除所有 授权缓存
     */
    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }

    /**
     * 自定义方法:清除所有 认证缓存
     */
    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }

    /**
     * 自定义方法:清除所有的  认证缓存  和 授权缓存
     */
    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }
}

(3)编写登录接口
用户名密码登录接口实现

/**
      * 用户名密码登录接口,地址需要与shiro的登录地址一一对应
      * 登录后浏览器传过来的session与被后台记住,session与对应用户进行了绑定
      * 可以通过SecurityUtils.getSubject();来获取当前用户session对应的认证信息
      * @param loginName
      * @param password
      * @param request
      * @param session
      * @param response
      * @return
      */
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    @ResponseBody
    public ReturnValue<String> login(@RequestParam("userName") String loginName,
                                        @RequestParam("password") String password) {  
        // 把前端输入的username和password封装为token
     // 使用realm指定的盐值+password进行配置的算法类型加密(加密次数也是配置),然后与数据库存储的秘钥解密后进行匹配(根据存储为HEX或Base64解密)
        //UsernamePasswordToken token = new UsernamePasswordToken(loginName, EncryptUtil.md5(password));
        UsernamePasswordToken token = new UsernamePasswordToken(loginName, password);
        // 认证身份
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            log.info("******登陆成功******");
           
            // 设置session时间
            //SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);
            String sessionId = subject.getSession().getId().toString();
            User user = (User) subject.getPrincipal();
            log.info("{}您好,欢迎光临!", user.getUsername());
           
            // 登录成功则返回token,用于无状态会话
            return new ReturnValue<String>(sessionId);
        } catch (AccountException e){
          log.info("******登录失败******");
          return new ReturnValue<String>(ErrorCode.ERROR_OBJECT_EXIST, e.getMessage());
        } catch (Exception e) {
          log.info("******未知错误******");
          return new ReturnValue<String>(ErrorCode.ERROR_SERVER_ERROR, "未知错误,用户登录失败,请联系管理员!");
        }
    }

用户手机登录的接口实现如下(注意这里没有实现短信发送,这个比较简单自己实现存储session即可):

/**
     * 通过手机验证码登录,,地址需要与shiro的登录地址一一对应(开放对应地址)
     * @param phone
     * @param code
     * @return
     */
    @RequestMapping(value = "/loginByhone", method = RequestMethod.GET)
    @ResponseBody
    public ReturnValue<String> loginByhone(@RequestParam("phone") String phone,
                                               @RequestParam("code") String code) {  
        // 把前端输入的phone和code封装为token
        CellphoneToken token = new CellphoneToken(phone, code);
        // 认证身份
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
                       
            // 设置session时间
            //SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);
            String sessionId = subject.getSession().getId().toString();
            User user = (User) subject.getPrincipal();
            log.info("{}您好,欢迎光临!", user.getUsername());
           
            // 登录成功则返回token,用于无状态会话
            return new ReturnValue<String>(sessionId);
        } catch (AccountException e){
          log.info("******登录失败******");
          return new ReturnValue<String>(ErrorCode.ERROR_OBJECT_EXIST, e.getMessage());
        } catch (Exception e) {
          log.info("******未知错误******");
          return new ReturnValue<String>(ErrorCode.ERROR_SERVER_ERROR, "未知错误,用户登录失败,请联系管理员!");
        }
    }

注意,这里返回Principal信息给shiro的时候,该接口必须实现序列化接口,否则报错,因为我这里用的user,所以实现如下

package com.dondown.model;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

import lombok.Data;

@Data
@JsonInclude(Include.NON_NULL)
public class User implements Serializable{
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    private Byte sex;
    private String certificate;
    private String wechat;
    private String username;
    private String password;
    private String mobile;
    private String email;
    private Byte enabled;
    private Byte expired;
    private Byte locked;
    private Integer reserver1;
    private Integer reserver2;
    private String reserver3;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        User other = (User) obj;
        if (certificate == null) {
            if (other.certificate != null)
                return false;
        } else if (!certificate.equals(other.certificate))
            return false;
        if (email == null) {
            if (other.email != null)
                return false;
        } else if (!email.equals(other.email))
            return false;
        if (enabled == null) {
            if (other.enabled != null)
                return false;
        } else if (!enabled.equals(other.enabled))
            return false;
        if (expired == null) {
            if (other.expired != null)
                return false;
        } else if (!expired.equals(other.expired))
            return false;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        if (locked == null) {
            if (other.locked != null)
                return false;
        } else if (!locked.equals(other.locked))
            return false;
        if (mobile == null) {
            if (other.mobile != null)
                return false;
        } else if (!mobile.equals(other.mobile))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (password == null) {
            if (other.password != null)
                return false;
        } else if (!password.equals(other.password))
            return false;
        if (reserver1 == null) {
            if (other.reserver1 != null)
                return false;
        } else if (!reserver1.equals(other.reserver1))
            return false;
        if (reserver2 == null) {
            if (other.reserver2 != null)
                return false;
        } else if (!reserver2.equals(other.reserver2))
            return false;
        if (reserver3 == null) {
            if (other.reserver3 != null)
                return false;
        } else if (!reserver3.equals(other.reserver3))
            return false;
        if (sex == null) {
            if (other.sex != null)
                return false;
        } else if (!sex.equals(other.sex))
            return false;
        if (username == null) {
            if (other.username != null)
                return false;
        } else if (!username.equals(other.username))
            return false;
        if (wechat == null) {
            if (other.wechat != null)
                return false;
        } else if (!wechat.equals(other.wechat))
            return false;
        return true;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((certificate == null) ? 0 : certificate.hashCode());
        result = prime * result + ((email == null) ? 0 : email.hashCode());
        result = prime * result + ((enabled == null) ? 0 : enabled.hashCode());
        result = prime * result + ((expired == null) ? 0 : expired.hashCode());
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        result = prime * result + ((locked == null) ? 0 : locked.hashCode());
        result = prime * result + ((mobile == null) ? 0 : mobile.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((password == null) ? 0 : password.hashCode());
        result = prime * result + ((reserver1 == null) ? 0 : reserver1.hashCode());
        result = prime * result + ((reserver2 == null) ? 0 : reserver2.hashCode());
        result = prime * result + ((reserver3 == null) ? 0 : reserver3.hashCode());
        result = prime * result + ((sex == null) ? 0 : sex.hashCode());
        result = prime * result + ((username == null) ? 0 : username.hashCode());
        result = prime * result + ((wechat == null) ? 0 : wechat.hashCode());
        return result;
    }
}

(4)编写系统配置添加所有自定义realm
添加用户名密码realm

   /**
      * 自定义用户名密码领域模型,从自定义模块加载用户信息、角色信息以及权限信息
      * @return
      */
     @Bean("userRealm")
     public ShiroUserRealm userRealm(@Qualifier("passwordHashedCredentialsMatcher") PasswordCredentialHashMatcher matcher) {
          ShiroUserRealm shiroRealm = new ShiroUserRealm();
          shiroRealm.setCachingEnabled(true);
         // 启用认证缓存,即缓存AuthenticationInfo信息,默认false
         shiroRealm.setAuthenticationCachingEnabled(true);
         // 缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
         //shiroRealm.setAuthenticationCacheName("authenticationCache");
         // 启用授权缓存,即缓存AuthorizationInfo信息,默认false
         shiroRealm.setAuthorizationCachingEnabled(true);
         // 缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置
         //shiroRealm.setAuthorizationCacheName("authorizationCache");
         // 配置自定义密码比较器
         shiroRealm.setCredentialsMatcher(matcher);
         return shiroRealm;
     }

添加手机登录realm

     /**
      * 自定义手机密码领域模型,从自定义模块加载用户信息、角色信息以及权限信息
      * @return
      */
     @Bean("phoneRealm")
     public ShiroCellphoneRealm phoneRealm(@Qualifier("cellphoneHashedCredentialsMatcher") CellphoneCredentialHashMatcher matcher) {
          ShiroCellphoneRealm cellphoneRealm = new ShiroCellphoneRealm();
          cellphoneRealm.setCachingEnabled(true);
         // 启用认证缓存,即缓存AuthenticationInfo信息,默认false
         cellphoneRealm.setAuthenticationCachingEnabled(true);
         // 缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
         //shiroRealm.setAuthenticationCacheName("authenticationCache");
         // 启用授权缓存,即缓存AuthorizationInfo信息,默认false
         cellphoneRealm.setAuthorizationCachingEnabled(true);
         // 缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置
         //shiroRealm.setAuthorizationCacheName("authorizationCache");
         // 配置自定义密码比较器
         cellphoneRealm.setCredentialsMatcher(matcher);
         return cellphoneRealm;
     }

上面的密码比较器,因为使用场景不同,可以有不同的实现

    /**
      * 密码散列算法:数据库应该保存规则与设置的相同
     * 密码校验规则HashedCredentialsMatcher
     * 这个类是为了对密码进行编码的 ,
     * 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
     * 这个类也负责对form里输入的密码进行编码
     * 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
     */
    @Bean("passwordHashedCredentialsMatcher")
    public PasswordCredentialHashMatcher passwordCredentialHashMatcher() {
     PasswordCredentialHashMatcher credentialsMatcher = new PasswordCredentialHashMatcher();
        // 指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置密码散列(加密)次数:md5(md5())
        credentialsMatcher.setHashIterations(2);
        // true密码加密用的是Hex编码;false时用Base64编码
        credentialsMatcher.setStoredCredentialsHexEncoded(false);
        return credentialsMatcher;
    }

手机验证码比较器

    /**
      * 手机密码匹配器
     */
    @Bean("cellphoneHashedCredentialsMatcher")
    public CellphoneCredentialHashMatcher phoneCredentialHashMatcher() {
     CellphoneCredentialHashMatcher credentialsMatcher = new CellphoneCredentialHashMatcher();
        // 指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置密码散列(加密)次数:md5(md5())
        credentialsMatcher.setHashIterations(2);
        // true密码加密用的是Hex编码;false时用Base64编码
        credentialsMatcher.setStoredCredentialsHexEncoded(false);
        return credentialsMatcher;
    }

注意,短信的验证必须根据实际场景生成获取以及比对。

因为这里有多个realm,我们都必须注入到SecurityManager中

/**
      * 注入shiro安全管理器设置realm认证
      * @return
      */
     @Bean("securityManager")
     public org.apache.shiro.mgt.SecurityManager securityManager(
              @Qualifier("userRealm") ShiroUserRealm userRealm,
              @Qualifier("phoneRealm") ShiroCellphoneRealm phoneRealm) {
          DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
          // 设置realm数据源
          //securityManager.setRealm(userRealm);
          List<Realm> realms = new ArrayList<Realm>();
          realms.add(userRealm);
          realms.add(phoneRealm);
          securityManager.setRealms(realms);
          
          // 注入ehcache缓存管理器;
          //securityManager.setCacheManager(ehCacheManager());
          securityManager.setCacheManager(redisCacheManager());
          // 注入shiro自带的内存缓存管理器
          //securityManager.setCacheManager(memoryCacheManager());
          // 注入Cookie记住我管理器
          // securityManager.setRememberMeManager(rememberMeManager());
          // 自定义的shiro session 缓存管理器实现前后端分离无状态会话-自定义token而不是使用浏览器session
          securityManager.setSessionManager(sessionManager());
          
          return securityManager;
     }

(5)自定义密码比较器实现
用户名密码比较器实现

package com.dondown.matcher;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SaltedAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;

import com.dondown.util.EncryptUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PasswordCredentialHashMatcher extends HashedCredentialsMatcher{
    
    /**
     * 自定义秘钥比较
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        log.debug("开始比较用户短信认证信息..");
        
        // 数据库存储密码--加密后再编码为base264后存储
        String db_pass = (String) info.getCredentials();
        
        // 用户名密码验证信息
        String login_pass = "";
        if(token instanceof UsernamePasswordToken){
            login_pass = new String((char[])token.getCredentials());
        }
        
        // 根据配置的存储类型进行转化
        if(isStoredCredentialsHexEncoded()){
            db_pass = new String(Hex.decode(db_pass));
        } else {
            db_pass = new String(Base64.decode(db_pass));
        }
        
        // 将登录密码加盐后比较
        String login_pass_result = EncryptUtil.md5(login_pass, ((SaltedAuthenticationInfo) info).getCredentialsSalt(), this.getHashIterations());
        return db_pass.trim().equals(login_pass_result);
    }

}

手机验证码比较器实现:

package com.dondown.matcher;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CellphoneCredentialHashMatcher extends HashedCredentialsMatcher{
    
    /**
     * 自定义秘钥比较
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        log.debug("开始比较用户密码认证信息..");
        
        String code = (String) token.getCredentials();
        
        // 实际生产过程中从redis或session中获取对应短信信息
        // User user = (User)info.getPrincipals();
        // String code = redisTemplate.get(user.getUsername());
        
        // 这里写死用于简单测试
        return code.equals("123");
    }

}

(6)对登录接口和手机登录接口开启访问权限(shiro配置

 /**
      * 定义shiroFilter过滤器并注入securityManager
      * ShiroFilterFactoryBean 处理拦截资源文件问题。
      * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
      * @param securityManager
      * @return
      */
     @Bean
     public ShiroFilterFactoryBean shirFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
         // shiroFilterFactoryBean对象
          ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
          // 配置shiro安全管理器 SecurityManager
          shiroFilterFactoryBean.setSecurityManager(securityManager);
          // 指定要求登录时的链接
          shiroFilterFactoryBean.setLoginUrl("/login");
          // 登录成功后要跳转的链接
          shiroFilterFactoryBean.setSuccessUrl("/index");
          // 未授权时跳转的界面;
          //shiroFilterFactoryBean.setUnauthorizedUrl("/403");
          
          // 配置拦截器.
          Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
          // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
          filterChainDefinitionMap.put("/logout", "anon");
          filterChainDefinitionMap.put("/loginByhone", "anon");
          filterChainDefinitionMap.put("/afterlogout", "anon");
          
          // 配置访问权限
          // 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
          // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
          filterChainDefinitionMap.put("/static/**", "anon");
          filterChainDefinitionMap.put("/templates/**", "anon");
          filterChainDefinitionMap.put("/swagger-*/**", "anon");
          filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
          filterChainDefinitionMap.put("/webjars/**", "anon");
          filterChainDefinitionMap.put("/v2/**", "anon");
          filterChainDefinitionMap.put("/afterlogin", "anon");
          // add操作,该用户必须有【addOperation】权限
          // filterChainDefinitionMap.put("/add", "perms[addOperation]");
          // 表示admin权限才可以访问
          // filterChainDefinitionMap.put("/admin/**", "roles[admin]");
          filterChainDefinitionMap.put("/**", "authc");
          filterChainDefinitionMap.put("/**/*", "authc");
     
          // 拦截器工厂类注入
          shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
          
          // 自定义拦截器限制并发人数
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        // 统计登录人数:限制同一帐号同时在线的个数
        //filtersMap.put("kickout", kickoutSessionControlFilter());
        // 自定义跨域前后端分离验证过滤器-自定义token情况
        filtersMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
       
          return shiroFilterFactoryBean;
     }

(7)系统配置

server:
  port: 7004
  servlet:
    context-path: /shiro
spring:
  application:
    name: shiro
  #NOSQL
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    timeout: 6000
    pool:
      max-active: 20   #连接池最大连接数(使用负值表示没有限制)
      max-wait: -1
      max-idle: 8
      min-idle: 0
  datasource:
    name: shiro
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/shiro?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
      username: root
      password: root
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 'x'
      validation-query-timeout: 6
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
mybatis:
  mapper-locations: classpath:mapping/*.xml
  type-aliases-package: com.dondown.model
endpoints:
  health:
    sensitive: false
    enabled: false
  shutdown:
    enabled: true
    path: /shutdown
    sensitive: false
management:
  security:
    enabled: false
logging:
  config: classpath:logback.xml
  level:
    org:
      springframework:
        web: INFO
        security: DEBUG
    #mybatis日志-接口所在包
    com.dondown.mapper: DEBUG
    #shiro日志-接口所在包
    org.crazycake.shiro: DEBUG
    org.apache.shiro: DEBUG
#actuator:
info:
  author:
    name: 李祥祥
    email: lixiang6153@126.com
  hostory:
  - date: 2018-08-28 10:10:10
    user: lixiang6153@126.com
  - date: 2018-07-10 08:30:00
    user: test@126.com
  build:
    artifact: "@project.artifactId@"
    name: "@project.name@"
    version: "@project.version@"

(8)测试
用户名登录测试
浏览器输入地址:

 http://localhost:7004/shiro/login?userName=lixx&password=dw123456

在这里插入图片描述
登录后查询用户信息:

http://localhost:7004/shiro/user/find/lixx

在这里插入图片描述
手机短信登录测试
输入地址:

http://localhost:7004/shiro/loginByhone?phone=18038856702&code=123

在这里插入图片描述
登录后查询用户信息:
在这里插入图片描述
后台地址打印(redis缓存):
在这里插入图片描述
(9)最后,送上我的pom配置文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.dondown</groupId>
  <artifactId>shiro-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
 
  <!-- spring boot项目 -->
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.0.4.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
  </parent>
 
  <!-- 项目属性:子模块不能引用父项目的properties变量 -->
  <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
      <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
      <!-- <spring-cloud.version>Finchley.BUILD-SNAPSHOT</spring-cloud.version>-->
      <lombok.version>1.16.20</lombok.version>
  </properties>
 
  <!-- 项目依赖管理声明,统一管理项目依赖的版本信息,继承项目无需声明版本 -->
  <dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${spring-cloud.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
          <!-- jdbc -->
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-jdbc</artifactId>
              <version>${spring.version}</version>
          </dependency>
          <!-- Spring-Mybatis -->
          <dependency>
              <groupId>org.mybatis.spring.boot</groupId>
               <artifactId>mybatis-spring-boot-starter</artifactId>
              <version>1.3.2</version>
          </dependency> 
         <!-- 阿里巴巴druid数据库连接池 -->
          <dependency>
              <groupId>com.alibaba</groupId>
               <artifactId>druid-spring-boot-starter</artifactId>
              <version>1.1.9</version>
          </dependency>
          <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
          </dependency>
          <!-- 分布式日志采集 -->
         <dependency>
              <groupId>net.logstash.logback</groupId>
               <artifactId>logstash-logback-encoder</artifactId>
              <version>4.9</version>
          </dependency>
      </dependencies>
  </dependencyManagement>
  <!-- 项目依赖:特殊强制依赖,其他继承父亲 -->
  <dependencies>
     <!--spring boot测试-->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
     </dependency>
     <!--Lombok:消除模板代码-->
     <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
     </dependency>
     <!-- logback日志包 -->
     <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
    </dependency>
    <!-- 分布式日志采集 -->
    <dependency>
          <groupId>net.logstash.logback</groupId>
          <artifactId>logstash-logback-encoder</artifactId>
     </dependency>
     <!-- SpringMVC -->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <!-- jdbc -->
     <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
     </dependency>
     <!-- MySQL -->
     <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
     </dependency>
     <!-- Spring-Mybatis -->
     <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
     </dependency> 
    <!-- 阿里巴巴druid数据库连接池 -->
     <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
     </dependency>
     <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid</artifactId>
     </dependency>
     <!-- 支持 @ConfigurationProperties配置文件读取 --> 
     <dependency> 
         <groupId>org.springframework.boot</groupId> 
         <artifactId>spring-boot-configuration-processor</artifactId> 
         <optional>true</optional> 
     </dependency>
     <!-- 权限校验 -->
     <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
     <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-spring</artifactId>
         <version>1.4.0</version>
     </dependency>
     <!-- ehcache权限缓存-->
     <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-ehcache</artifactId>
         <version>1.3.2</version>
     </dependency>
     <!-- reids作为缓存 -->
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- shiro-redis2.4.2.1-RELEASE或3.1.0 -->
     <dependency>
         <groupId>org.crazycake</groupId>
         <artifactId>shiro-redis</artifactId>
         <version>2.4.2.1-RELEASE</version>
     </dependency>
     <!-- 解决:eclipse报错:Missing artifact com.sun:tools:jar 1.8.0 -->
     <dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>1.8</version>
        <scope>system</scope>
        <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
    </dependency>
     <!-- fastjson阿里巴巴jSON处理器 -->
    <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.58</version>
     </dependency>
  </dependencies>
  <!-- 编译插件 -->
  <build>
      <plugins>
            <!--spring boot maven插件-->
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
          <!-- mybatis generator 自动生成代码插件[实体和mapper],编译命令: mybatis-generator:generate -e -->
           <plugin>
               <groupId>org.mybatis.generator</groupId>
               <artifactId>mybatis-generator-maven-plugin</artifactId>
               <version>1.3.2</version>
               <configuration>
                   <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
                   <overwrite>true</overwrite>
                   <verbose>true</verbose>
               </configuration>
           </plugin>
      </plugins>
  </build>
 
</project>

快来成为我的朋友或合作伙伴,一起交流,一起进步!:
QQ群:961179337
微信:lixiang6153
邮箱:lixx2048@163.com
公众号:IT技术快餐

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝壳里的沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值