一、前言
在登录认证中,用户名密码登录到系统是必然的模式,但是很多系统或日常中,可以有很多其他身份登陆到对应系统中,比如手机号码、邮箱、微信等,在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技术快餐