shiro的身份认证的流程,大致是这样的:当我们调用subject.login(token)的时候,首先这次身份认证会委托给Security Manager,而Security Manager又会委托给Authenticator,接着Authenticator会把传过来的token再交给我们自己注入的Realm进行数据匹配从而完成整个认证。如果不太了解这个流程建议再仔细读一下官方提供的Authentication说明文档:
http://shiro.apache.org/authentication.html
我们输入用户名和密码点击submit则跳到UserController执行登录的业务逻辑,接下来看看UserController的代码:
主要看逻辑即可
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public String login(@Valid TUser tuser, BindingResult result, Model model, HttpServletRequest request, HttpServletResponse response) {
if (request.getMethod().equals(RequestMethod.GET.toString())) {
//return "login";
return JSON.toJSONString(ResultUtil.error("100", "请求路径错误"));
}
TDict dict = null;
Map<String, Object> map = new HashedMap();
try {
Subject subject = SecurityUtils.getSubject();
TUser user = userService.getUserByName(tuser.getUsername(), 0L);
// 已登陆则 跳到首页
if (subject.isAuthenticated()) {
// return "redirect:/";
return JSON.toJSONString(ResultUtil.success("200", "已登录"));
}
if (result.hasErrors()) {
model.addAttribute("error", "* 用户名或密码错误!");
return JSON.toJSONString(ResultUtil.error("100", "用户名或密码错误"));
}
// 身份验证
subject.login(new UserToken(tuser.getUsername(), tuser.getPassword(), true));
// 验证成功在Session中保存用户信息
final TUser authUserInfo = userService.getUserByName(tuser.getUsername(), tuser.getType());
//最后一次登录时间
authUserInfo.setLastlogin(new Date());
userService.update(authUserInfo);
request.getSession().setAttribute("userInfo" + authUserInfo.getUsername(), authUserInfo);
request.getSession().setAttribute("user", authUserInfo);
dict = dictService.selectDictByKey(Constant.IS_POLICY_USE);
if (dict == null) {
dict = new TDict();
dict.setValue("0");
}
List<TRole> roleInfos = roleService.getRoleByIdList(user.getUserroles());
List<TPermission> permissions = new ArrayList<TPermission>();
if(roleInfos != null && roleInfos.size() > 0){
for (TRole role : roleInfos) {
permissions = permissionService.getPermissionsByIdList(role.getRolepermissions());
}
}
Map<String, Object> permissionmap = new HashedMap();
//数据处理
dataDeal(permissions, permissionmap);
map.put("permissions", permissionmap);
request.getSession().setAttribute("dic", dict.getValue());
if (roleInfos != null){
request.getSession().setAttribute("role", roleInfos.get(0));
}
}catch (ExcessiveAttemptsException e) {
return JSON.toJSONString(ResultUtil.error("100", "登录失败超过5次,请10分钟后再次尝试"));
}
catch (LockedAccountException e) {
return JSON.toJSONString(ResultUtil.error("100", "账号被锁定"));
}
catch (AuthenticationException e) {
// 身份验证失败
model.addAttribute("error", "* 用户名或密码错误 !");
//return "login";
return JSON.toJSONString(ResultUtil.error("100", "用户名或密码错误"));
}
// redirectAttributes.addFlashAttribute("message","用户名或密码错误");
logger.info(tuser.getUsername() + " 登录系统。");
logService.saveAdmin(AdminOperationType.LOGIN);
// return "redirect:/#Dashboard";
// Map<String, String> dic = new HashedMap();
map.put(dict.getKey(), dict.getValue());
map.put("userName", tuser.getUsername());
return JSON.toJSONString(ResultUtil.success("200", map));
}
根据shiro的认证流程,最终Authenticator会把login传入的参数token交给Realm进行验证,Realm往往也是我们自己注入的,我们在debug模式下不难发现,在subject.login(token)打上断点,F6之后会跳到我们Realm类中doGetAuthenticationInfo(AuthenticationToken token)这个回调方法,从而也验证了认证流程确实没问题。下面贴出Realm中的代码:
package cn.easted.edm.core.security;
import java.util.List;
import javax.annotation.Resource;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.apache.shiro.util.ByteSource;
import cn.easted.edm.core.generic.enums.UserSessionStatus;
import cn.easted.edm.core.generic.enums.UserType;
import cn.easted.edm.core.model.TPermission;
import cn.easted.edm.core.model.TRole;
import cn.easted.edm.core.model.TUser;
import cn.easted.edm.core.service.PermissionService;
import cn.easted.edm.core.service.RoleService;
import cn.easted.edm.core.service.UserService;
import cn.easted.edm.core.utils.DESUtil;
/**
* 用户身份验证,授权 Realm 组件
**/
public class SecurityRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
/**
* 权限检查
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = String.valueOf(principals.getPrimaryPrincipal());
final TUser user = userService.getUserByName(username, 0L);
final List<TRole> roleInfos = roleService.getRoleByIdList(user.getUserroles());
for (TRole role : roleInfos) {
// 添加角色
authorizationInfo.addRole(role.getRoleSign());
final List<TPermission> permissions = permissionService.getPermissionsByIdList(role.getRolepermissions());
for (TPermission permission : permissions) {
// 添加权限
authorizationInfo.addStringPermission(permission.getPermissionSign());
}
}
return authorizationInfo;
}
/**
* 登录验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authtoken) throws AuthenticationException {
UserToken token = (UserToken) authtoken;
String username = String.valueOf(token.getPrincipal());
// String password = new String((char[]) token.getCredentials());
String key = token.getKey();
String signature = token.getSignature();
String domain = token.getDomain();
TUser authentication = null;
if (StringUtils.isEmpty(key) && StringUtils.isEmpty(signature)) {
// 通过数据库进行验证
// authentication = userService.userValidate(username, password, UserType.LOCAL.getValue());
authentication = userService.getUserByName(username, UserType.LOCAL.getValue());
if (authentication == null) {
throw new AuthenticationException("用户名或密码错误");
}
// 判断帐号是否锁定
if (Boolean.TRUE.equals(authentication.getIslock() == 1 ? true : false)) {
// 抛出 帐号锁定异常
throw new LockedAccountException();
}
} else {
try {
if (signature.equals(DESUtil.encrypt(key, ""))) {
userService.updateSessionstatusStatus(username, domain, UserSessionStatus.ONLINE);
} else {
throw new AuthenticationException();
}
} catch (Exception e) {
e.printStackTrace();
// 身份验证失败
throw new AuthenticationException("501");
}
List<TUser> users = userService.getUserByNameDomain(username, domain);
if (null == users || users.size() < 1) {
// 用户名域验证失败
throw new AuthenticationException("502");
}
// authentication=userService.userValidateForApi(username, eid,
// userUUID, domain);
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username,
authentication.getPassword(),
ByteSource.Util.bytes(username),
getName());
return authenticationInfo;
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
关于Realm我们一般都会继承AuthorizingRealm去实现我们自己的Realm类,虽然从名字看这个Realm是用于授权的,而我们此处需要用到的是身份认证,但实际上AuthorizingRealm也继承了AuthenticatingRealm。
UserToken.java
package cn.easted.edm.core.security;
import java.util.Arrays;
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* @Date: 2017年3月2日 下午5:08:09
*/
public class UserToken extends UsernamePasswordToken {
/**
* @Fields serialVersionUID : TODO
*/
private static final long serialVersionUID = 1L;
private String username;
private char[] password;
private String key;
private String signature;
private String domain;
private boolean rememberMe = false;
private String host;
private String salt;
public UserToken() {
}
public UserToken(final String username, final String password, final String key, final String signature,
final String domain) {
this(username, password != null ? password.toCharArray() : null, key, signature, domain, true);
}
public UserToken(final String username, final String password, final boolean rememberMe) {
this(username, password != null ? password.toCharArray() : null, null, null, null, rememberMe);
}
public UserToken(final String username, final char[] password, final String key, final String signature,
final String domain, Boolean rememberMe) {
this.username = username;
this.password = password;
this.key = key;
this.signature = signature;
this.domain = domain;
this.rememberMe = rememberMe;
}
/*
* (non-Javadoc)
*
* @see org.apache.shiro.authc.AuthenticationToken#getPrincipal()
*/
@Override
public Object getPrincipal() {
return getUsername();
}
/*
* (non-Javadoc)
*
* @see org.apache.shiro.authc.AuthenticationToken#getCredentials()
*/
@Override
public Object getCredentials() {
return getPassword();
}
/*
* (non-Javadoc)
*
* @see org.apache.shiro.authc.RememberMeAuthenticationToken#isRememberMe()
*/
@Override
public boolean isRememberMe() {
return rememberMe;
}
/*
* (non-Javadoc)
*
* @see org.apache.shiro.authc.HostAuthenticationToken#getHost()
*/
@Override
public String getHost() {
return host;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username
* the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @param rememberMe
* the rememberMe to set
*/
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
/**
* @param host
* the host to set
*/
public void setHost(String host) {
this.host = host;
}
/**
* @return the password
*/
public char[] getPassword() {
return password;
}
/**
* @param password
* the password to set
*/
public void setPassword(char[] password) {
this.password = password;
}
/**
* @return the serialversionuid
*/
public static long getSerialversionuid() {
return serialVersionUID;
}
/**
* @return the domain
*/
public String getDomain() {
return domain;
}
/**
* @param domain
* the domain to set
*/
public void setDomain(String domain) {
this.domain = domain;
}
/**
* @return the key
*/
public String getKey() {
return key;
}
/**
* @param key
* the key to set
*/
public void setKey(String key) {
this.key = key;
}
/**
* @return the signature
*/
public String getSignature() {
return signature;
}
/**
* @param signature
* the signature to set
*/
public void setSignature(String signature) {
this.signature = signature;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "UserToken [username=" + username + ", password=" + Arrays.toString(password) + ", key=" + key
+ ", signature=" + signature + ", domain=" + domain + ", rememberMe=" + rememberMe + ", host=" + host
+ ", salt=" + salt + "]";
}
}
TUser.java
package cn.easted.edm.core.model;
import java.util.Date;
public class TUser {
private Long id;
private String username;
private String nickname;
private String password;
private Long status;
private Date createtime;
private String usergroups;
private String email;
private String phone;
private Long type;
private String userroles;
private String uuid;
private String domainentryid;
private String domain;
private Date lastlogin;
private String sessionstatus;
private Long version;
private String reserve;
private String ecenterid;
private Long islock;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname == null ? null : nickname.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public Long getStatus() {
return status;
}
public void setStatus(Long status) {
this.status = status;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public String getUsergroups() {
return usergroups;
}
public void setUsergroups(String usergroups) {
this.usergroups = usergroups == null ? null : usergroups.trim();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email == null ? null : email.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
public Long getType() {
return type;
}
public void setType(Long type) {
this.type = type;
}
public String getUserroles() {
return userroles;
}
public void setUserroles(String userroles) {
this.userroles = userroles == null ? null : userroles.trim();
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid == null ? null : uuid.trim();
}
public String getDomainentryid() {
return domainentryid;
}
public void setDomainentryid(String domainentryid) {
this.domainentryid = domainentryid == null ? null : domainentryid.trim();
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain == null ? null : domain.trim();
}
public Date getLastlogin() {
return lastlogin;
}
public void setLastlogin(Date lastlogin) {
this.lastlogin = lastlogin;
}
public String getSessionstatus() {
return sessionstatus;
}
public void setSessionstatus(String sessionstatus) {
this.sessionstatus = sessionstatus == null ? null : sessionstatus.trim();
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public String getReserve() {
return reserve;
}
public void setReserve(String reserve) {
this.reserve = reserve == null ? null : reserve.trim();
}
public String getEcenterid() {
return ecenterid;
}
public void setEcenterid(String ecenterid) {
this.ecenterid = ecenterid == null ? null : ecenterid.trim();
}
public Long getIslock() {
return islock;
}
public void setIslock(Long islock) {
this.islock = islock;
}
}
HashedCredentialsMatcher.java
package cn.easted.edm.core.security;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import cn.easted.edm.core.generic.enums.UserType;
import cn.easted.edm.core.model.TUser;
import cn.easted.edm.core.service.UserService;
/**
* 登陆次数验证
* @ClassName:RetryLimitHashedCredentialsMatcher
* @author:Wanghao
* @date: 2017年10月11日 上午11:29:58
*/
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher{
private static final Logger logger = Logger.getLogger(RetryLimitHashedCredentialsMatcher.class);
@Autowired
private UserService userservice;
private Cache<String, AtomicInteger> passwordRetryCache;
private Map<Long, String> map = new HashMap<Long, String>();
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
// retry count + 1
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
if (retryCount.incrementAndGet() > 5) {
TUser user = userservice.getUserByName(username, UserType.LOCAL.getValue());
if (user != null){
user.setIslock(1L);
userservice.update(user);
if (! map.containsValue(user.getUsername())){
map.put(System.currentTimeMillis(), user.getUsername());
}
}
logger.info("锁定用户" + user.getUsername());
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
passwordRetryCache.remove(username);
}
return matches;
}
/**
* @Title: getMap <BR>
* @return:Map<Long,TUser> <BR>
*/
public Map<Long, String> getMap() {
return map;
}
}
在回调方法doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info)中进行身份认证的密码匹配,这里我们引入了Ehcahe用于保存用户登录次数,如果登录失败retryCount变量则会一直累加,如果登录成功,那么这个count就会从缓存中移除,从而实现了如果登录次数超出指定的值就锁定。我们看一下spring的缓存配置和ehcache的配置:
<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
ehcache-shiro.xml
<ehcache name="shirocache" updateCheck="false">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="600"
/>
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryCache" eternal="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false"
statistics="true">
</cache>
<cache name="authorizationCache" eternal="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache" eternal="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache" eternal="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false" maxBytesLocalHeap="2000"
statistics="true">
</cache>
<!-- timeToIdleSeconds 此属性设置后,限制时间以上次访问开始
eg:设置时间为10分钟的话,我们在1分的时候账号被锁定,预定为10分时解锁.而我们在5分时再次输入密码(此时密码已被锁定,无论怎样都不会通过认证),那么解锁时间变为15分.
timeToLiveSeconds 此属性设置后,限制时间以缓存创建开始
eg:设置时间为10分钟的话,我们在1分的时候账号被锁定,预定为10分时解锁.而我们在5分时再次输入密码(此时密码已被锁定,无论怎样都不会通过认证),那么解锁时间还是10分.
maxBytesLocalHeap用来限制缓存所能使用的堆内存的最大字节数的,如果不设置则需设置另外一个属性,否则项目会编译出错,无法允许,此处不再详写。
-->
</ehcache>
我这里用的是2.4.8版本的ehcache:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.8</version>
</dependency>
下面再回到重点,密码是如何匹配的?我们在我们自定义的HashedCredentialsMatcher应该可以看到这样一个方法:
boolean matches = super.doCredentialsMatch(token, info);
显而易见,是通过这个方法进行密码验证的,如果成功,则清除ehcache中存储的记录登录失败次数的count。我们可以看到这个方法的两个参数,token和info,它们是回调方法:
boolean doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info) 由UserRealm传过来的参数,所以至于如何验证密码,其实还是由UserRealm返回的SimpleAuthenticationInfo决定的。HashedCredentialsMatcher允许我们指定自己的算法和盐,比如:我们采取加密的方法是(1024次md5迭代,用户名当作盐),通过shiro提供的通用散列来实现:
package cn.easted.edm.core.security;
import org.apache.shiro.crypto.hash.SimpleHash;
/**
* 生成密码
* @ClassName:GenerateMD5pwd
* @author:Wanghao
* @date: 2017年10月11日 上午11:45:36
*/
public class GenerateMD5pwd{
public static void main(String[] args) {
String hashAlgorithmName = "md5";
String username = "safer";
String password = "easted2013";
int hashIterations = 1024;
String salt = username;
SimpleHash hash = new SimpleHash(hashAlgorithmName, password,
salt, hashIterations);
String encodedPassword = hash.toHex();
System.out.println(encodedPassword);
}
}
我们输出密码,保存到数据库中模拟已经注册好的用户数据.
这样我们在Realm中调用UserService的时候就可以查询出密码和盐,最后通过SimpleAuthenticationInfo将它们组装起来即可,上面也提到了HashedCredentialsMatcher会自动识别这个盐。还有不要忘记算法要一致,即加密和匹配时的算法,如果我们采取上述main方法中的加密方式,那么我们需要给自定义的HashedCredentialsMatcher注入如下属性
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher"
class="cn.easted.edm.core.security.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="shiroEhcacheManager"/>
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1024"/>
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
<!-- Realm实现 -->
<bean id="securityRealm" class="cn.easted.edm.core.security.SecurityRealm">
<!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="cachingEnabled" value="true"/>
<property name="cacheManager" ref="shiroEhcacheManager"/>
</bean>