SpringBoot -- 整合Shiro实现用户登录认证

本文介绍了如何在SpringBoot项目中整合Shiro框架进行用户登录认证。首先,文章简述了Shiro的基本概念和优势,然后详细讲解了Shiro的内部结构和认证流程。接着,作者提供了添加Shiro依赖、配置类以及自定义UserRealm的步骤,并分享了在认证过程中遇到的问题及解决方案,如Ehcache缓存配置、分模块开发时的包扫描问题。最后,文章通过测试展示了Shiro的认证过程,并给出了项目的GitHub链接供读者参考学习。

前言:前段时间在搭建公司框架安全验证的时候,就想到之前web最火的shiro框架,所以就在空闲时间学习并总结一些搭建流程和解析,这里给大家出个简单的教程说明吧。

shiro的基本介绍这里就不再说了,可以自行百度相关的shiro教程,对于第一次接触shiro的小伙伴,博主推荐你可以先阅读该篇文章Shiro,对shiro进行初步的了解,以及它的核心原理。

一、什么是shiro?

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。使用shiro实现系统 的权限管理,有效提高开发效率,从而降低开发成本。

1.1 shiro内部结构框架  官网架构说明

å¨è¿éæå¥å¾çæè¿°

1.2 外部看shiro架构 

api说明
Subject主体,代表当前‘用户’ 。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委派给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者
SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互且它管理者所有Subject;可以看出它是Shiro的核心,它负责与后面介绍的其它组件进行交互以把它看成DispathcherServlet前端控制器
Realm域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

 1.3 shiro认证流程

 二、添加shiro相关依赖

<!--验证码 用于登录安全校验-->
<dependency>
	<groupId>com.github.penggle</groupId>
	<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
			
<!--Shiro核心框架 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>${shiro.version}</version>
</dependency>
			
<!-- Shiro使用Srping框架 -->
	<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>${shiro.version}</version>
</dependency>
			
<!-- Shiro使用EhCache缓存框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>${shiro.version}</version>
</dependency>
			
<!-- thymeleaf模板引擎和shiro框架的整合 -->
<dependency>
	<groupId>com.github.theborakompanioni</groupId>
	<artifactId>thymeleaf-extras-shiro</artifactId>
	<version>${thymeleaf.extras.shiro.version}</version>
</dependency>

三、添加相关配置类

在这里,博主先使用ehcache对用户登录信息做缓存管理(推荐使用reids做缓存好些,可解决分布式架构下session共享问题,可参考分布式架构中shiro的实现

ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="layduo" updateCheck="false">

    <!-- 磁盘缓存位置 -->
    <diskStore path="java.io.tmpdir"/>
    
    <!-- maxEntriesLocalHeap:堆内存中最大缓存对象数,0没有限制 -->
    <!-- maxElementsInMemory: 在内存中缓存的element的最大数目。-->
    <!-- eternal:elements是否永久有效,如果为true,timeouts将被忽略,element将永不过期 -->
    <!-- timeToIdleSeconds:失效前的空闲秒数,当eternal为false时,这个属性才有效,0为不限制 -->
    <!-- timeToLiveSeconds:失效前的存活秒数,创建时间到失效时间的间隔为存活时间,当eternal为false时,这个属性才有效,0为不限制 -->
    <!-- overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上 -->
    <!-- statistics:是否收集统计信息。如果需要监控缓存使用情况,应该打开这个选项。默认为关闭(统计会影响性能)。设置statistics="true"开启统计 -->
    
    <!-- 默认缓存 -->
    <defaultCache
            maxEntriesLocalHeap="1000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="false">
    </defaultCache>

    <!-- 登录记录缓存 锁定10分钟 -->
    <cache name="loginRecordCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <!-- 系统活跃用户缓存 -->
    <cache name="sys-userCache"
           maxEntriesLocalHeap="10000"
           overflowToDisk="false"
           eternal="false"
           diskPersistent="false"
           timeToLiveSeconds="0"
           timeToIdleSeconds="0"
           statistics="true">
    </cache>

    <!-- 系统会话缓存 -->
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           overflowToDisk="true"
           eternal="true"
           timeToLiveSeconds="0"
           timeToIdleSeconds="0"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600">
    </cache>

</ehcache>
	

application.yml中shiro相关配置参数

#shiro
shiro:
   user:
      #登录地址
      loginUrl: /login
      #权限认证失败地址
      unauthorizedUrl: /unauth
      #首页地址
      indexUrl: /index
      #验证码开关
      captchaEnabled: true
      #验证码类型: math数据计算、char字符检验
      captchaType: math
   cookie:
      #设置cookie的域名 默认为空,即当前访问的域名
      domain:
      #设置cookie的有效访问路径
      path: /
      #设置HttpOnly属性
      httpOnly: true
      #设置cookie的过期时间,单位为天
      maxAge: 30
   session:
      #session超时时间,-1代表永不过期(默认为30分钟)
      expireTime: 30
      #同步session到数据库的周期(默认1分钟)
      dbSyncPeriod: 1
      #相隔多久检查一次session的有效性,默认10分钟
      validationInterval: 10
      #同一个用户最大会话数,比如1的意思是同一个账号允许最多同时一个人登录(默认-1不限制)
      maxSession: 1
      #踢出之前登录的/之后登录的用户,默认踢出之前登陆的用户
      kickoutAfter: false

shiro配置类 -- ShiroConfig.class

package com.layduo.framework.config;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

import org.apache.commons.io.IOUtils;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.layduo.common.utils.StringUtils;
import com.layduo.framework.shiro.realm.UserRealm;
import com.layduo.framework.shiro.web.filter.LogoutFilter;
import com.layduo.framework.shiro.web.filter.captcha.CaptchaValidateFilter;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

/**
 * shiro相关配置
 * 
 * @author layduo
 * @createTime 2019年11月7日 下午6:26:22
 */
@Configuration
public class ShiroConfig {

	public static final String PREMISSION_STRING = "perms[\"{0}\"]";

	// Session超时时间,单位为毫秒(默认30分钟)
	@Value("${shiro.session.expireTime}")
	private int expireTime;

	// 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
	@Value("${shiro.session.validationInterval}")
	private int validationInterval;

	// 同一个用户最大会话数
	@Value("${shiro.session.maxSession}")
	private int maxSession;

	// 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
	@Value("${shiro.session.kickoutAfter}")
	private boolean kickoutAfter;

	// 验证码开关
	@Value("${shiro.user.captchaEnabled}")
	private boolean captchaEnabled;

	// 验证码类型
	@Value("${shiro.user.captchaType}")
	private String captchaType;

	// 设置Cookie的域名
	@Value("${shiro.cookie.domain}")
	private String domain;

	// 设置cookie的有效访问路径
	@Value("${shiro.cookie.path}")
	private String path;

	// 设置HttpOnly属性
	@Value("${shiro.cookie.httpOnly}")
	private boolean httpOnly;

	// 设置Cookie的过期时间,秒为单位
	@Value("${shiro.cookie.maxAge}")
	private int maxAge;

	// 登录地址
	@Value("${shiro.user.loginUrl}")
	private String loginUrl;

	// 权限认证失败地址
	@Value("${shiro.user.unauthorizedUrl}")
	private String unauthorizedUrl;

	@Bean
	public EhCacheManager getEhCacheManager() {
		// 获取ehcache-shiro.xml里对应的缓存管理器
		net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.getCacheManager("layduo");
		EhCacheManager ehCacheManager = new EhCacheManager();
		if (StringUtils.isNull(cacheManager)) {
			ehCacheManager.setCacheManager(new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()));
		} else {
			ehCacheManager.setCacheManager(cacheManager);
		}
		return ehCacheManager;
	}

	/**
	 * 返回配置文件流 避免ehcache配置文件一直被占用,无法完全销毁项目重新部署
	 * 
	 * @return
	 */
	protected InputStream getCacheManagerConfigFileInputStream() {
		String configFile = "classpath:ehcache/ehcache-shiro.xml";
		InputStream inputStream = null;

		try {
			inputStream = ResourceUtils.getInputStreamForPath(configFile);
			byte[] b = IOUtils.toByteArray(inputStream);
			InputStream in = new ByteArrayInputStream(b);
			return in;
		} catch (IOException e) {
			// 无法获取cacheManagerConfigFile的输入流
			throw new ConfigurationException(
					"Unable to obtain input stream for cacheManagerConfigFile [" + configFile + "]", e);
		} finally {
			IOUtils.closeQuietly(inputStream);
		}
	}
	
	/**
	 * 自定义Realm 认证授权逻辑实现
	 * @param cacheManager
	 * @return
	 */
	@Bean
	public UserRealm userRealm(EhCacheManager cacheManager) {
		UserRealm userRealm = new UserRealm();
		userRealm.setCacheManager(cacheManager);
		return userRealm;
	}
	
	/**
	 * 安全管理器
	 * @param userRealm
	 * @return
	 */
	@Bean
	public SecurityManager securityManager(UserRealm userRealm) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		//设置realm
		securityManager.setRealm(userRealm);
		//注入缓存管理器
		securityManager.setCacheManager(getEhCacheManager());
		return securityManager;
	}
	
	/**
	 * Shiro过滤器配置 、配置权限拦截规则指定跳转页面
	 * @param securityManager
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		//Shiro的核心安全接口,这个属性是必须的
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		//身份认证失败,则跳转到登录页面的配置
		shiroFilterFactoryBean.setLoginUrl(loginUrl);
		//权限认证失败,则跳转到指定页面
		shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
		//Shiro连接约束配置,即过滤链的定义
		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		//对静态资源设置匿名访问
		filterChainDefinitionMap.put("/favicon.ico**", "anon");
		filterChainDefinitionMap.put("/ruoyi.png**", "anon");
		filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/ruoyi/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
        //退出logout地址,shiro清除session
        filterChainDefinitionMap.put("/logout", "logout");
        //不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon,captchaValidate");
        
        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
        filters.put("captchaValidate", captchaValidateFilter());
        
        //注销成功,则跳转到指定页面
        filters.put("logout", logoutFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //所有请求需要认证
        filterChainDefinitionMap.put("/**", "user");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
	    return shiroFilterFactoryBean;
	}
	
	/**
	 * 退出过滤器
	 * @return
	 */
	public LogoutFilter logoutFilter() {
		LogoutFilter logoutFilter = new LogoutFilter();
		logoutFilter.setCacheManager(getEhCacheManager());
		logoutFilter.setLoginUrl(loginUrl);
		return logoutFilter;
	}
	
	/**
	 * 自定义验证码过滤器
	 * @return
	 */
	@Bean
	public CaptchaValidateFilter captchaValidateFilter() {
		CaptchaValidateFilter captchaValidateFilter = new CaptchaValidateFilter();
		captchaValidateFilter.setCaptchaEnabled(captchaEnabled);
		captchaValidateFilter.setCaptchaType(captchaType);
		return captchaValidateFilter;
	}
	
	/**
	 * cookie 属性设置 配置文件默认30天
	 * @return
	 */
	public SimpleCookie rememberMeCookie() {
		SimpleCookie cookie = new SimpleCookie("rememberMe");
		cookie.setDomain(domain);
		cookie.setPath(path);
		cookie.setHttpOnly(httpOnly);
		cookie.setMaxAge(maxAge * 24 * 60 * 60);
		return cookie;
	}
	
	/**
	 * 记住我
	 * @return
	 */
	public CookieRememberMeManager rememberMeManager() {
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		cookieRememberMeManager.setCookie(rememberMeCookie());
		cookieRememberMeManager.setCipherKey(Base64.decode("fCq+/xW488hMTCD+cmJ3aQ=="));
		return cookieRememberMeManager;
	}
	
	/**
	 * thymeleaf模板引擎和shiro框架整合
	 * @return
	 */
	@Bean
	public ShiroDialect shiroDialect() {
		return new ShiroDialect();
	}
	
	/**
	 * 开启Shiro注解通知器
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
}

自定义UserRealm认证授权逻辑实现

package com.layduo.framework.shiro.realm;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.layduo.common.exception.user.CaptchaException;
import com.layduo.common.exception.user.RoleBlockedException;
import com.layduo.common.exception.user.UserBlockedException;
import com.layduo.common.exception.user.UserNotExistsException;
import com.layduo.common.exception.user.UserPasswordNotMatchException;
import com.layduo.common.exception.user.UserPasswordRetryLimitExceedException;
import com.layduo.common.utils.StringUtils;
import com.layduo.framework.shiro.service.SysLoginService;
import com.layduo.system.domain.SysUser;

/**
 * 自定义Realm处理登录认证授权
 * 
 * @author layduo
 * @createTime 2019年11月8日 上午10:18:12
 */
public class UserRealm extends AuthorizingRealm {

	private static final Logger log = LoggerFactory.getLogger(UserRealm.class);

	@Autowired
	private SysLoginService loginService;

	/**
	 * 授权
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //整合授权时讲述
		return null;
	}

	/**
	 * 用户认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		String username = upToken.getUsername();
		String password = StringUtils.EMPTY;
		if (upToken.getPassword() != null) {
			password = new String(upToken.getPassword());
		}
		SysUser user = null;
		try {
			user = loginService.login(username, password);
		} catch (CaptchaException e) {
			throw new AuthenticationException(e.getMessage(), e);
		} catch (UserNotExistsException e) {
			throw new UnknownAccountException(e.getMessage(), e);
		} catch (UserPasswordNotMatchException e) {
			throw new IncorrectCredentialsException(e.getMessage(), e);
		} catch (UserPasswordRetryLimitExceedException e) {
			throw new ExcessiveAttemptsException(e.getMessage(), e);
		} catch (UserBlockedException e) {
			throw new LockedAccountException(e.getMessage(), e);
		} catch (RoleBlockedException e) {
			throw new LockedAccountException(e.getMessage(), e);
		} catch (Exception e) {
			log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
			throw new AuthenticationException(e.getMessage(), e);
		}
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
		return info;
	}

	/**
	 * 清理缓存权限
	 */
	public void clearCachedAuthorizationInfo() {
		this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
	}

}

调用数据库查询实现具体认证过程,开发过程如果想跳过验证码校验,可以在全局配置文件application.yml中,把验证码开关设置为false

package com.layduo.framework.shiro.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.layduo.common.constant.Constants;
import com.layduo.common.constant.ShiroConstants;
import com.layduo.common.constant.UserConstants;
import com.layduo.common.enums.UserStatus;
import com.layduo.common.exception.user.CaptchaException;
import com.layduo.common.exception.user.UserBlockedException;
import com.layduo.common.exception.user.UserDeleteException;
import com.layduo.common.exception.user.UserNotExistsException;
import com.layduo.common.exception.user.UserPasswordNotMatchException;
import com.layduo.common.utils.DateUtils;
import com.layduo.common.utils.MessageUtils;
import com.layduo.common.utils.ServletUtils;
import com.layduo.framework.manager.AsyncManager;
import com.layduo.framework.manager.factory.AsyncFactory;
import com.layduo.framework.util.ShiroUtils;
import com.layduo.system.domain.SysUser;
import com.layduo.system.service.ISysUserService;

/**
 * 登录校验方法
* @author layduo
* @createTime 2019年11月11日 上午10:23:46
*/
@Component
public class SysLoginService {

	@Autowired
	private SysPasswordService passwordService;
	
	@Autowired
	private ISysUserService userService;
	
	/**
	 * 登录
	 * @param username
	 * @param password
	 * @return
	 */
	public SysUser login(String username, String password) {
		
		//验证码校验
		String captacha = (String) ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA);
		if (!StringUtils.isEmpty(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA))) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
			throw new CaptchaException();
		}
		//用户名或密码为空 错误
		if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
			throw new UserNotExistsException();
		}
		//用户名不在指定范围内 错误
		if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
			throw new UserPasswordNotMatchException();
		}
		//密码不在指定范围内 错误
		if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
			throw new UserPasswordNotMatchException();
		}
		//查询用户信息
		SysUser user = userService.selectUserByLoginName(username);
		
		//用手机号码登录
		if (user == null && maybeMobilePhoneNumber(username)) {
			user = userService.selectUserByPhoneNumber(username);
		}
		//用电子邮箱登录
		if (user == null && maybeEmail(username)) {
			user = userService.selectUserByEmail(username);
		}
		//校验用户不存在
		if (user == null) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
			throw new UserNotExistsException();
		}
		//校验用户删除状态
		if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.delete")));
			throw new UserDeleteException();
		}
		//校验用户状态
		if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark())));
			throw new UserBlockedException();
		}
		
		//校验用户密码
		passwordService.validate(user, password);
		
		//校验成功,添加登录记录
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
		//记录用户登录IP和时间
		recordLoginInfo(user);
		return user;
		
	}
	
	private boolean maybeEmail(String username) {
		if (!username.matches(UserConstants.EMAIL_PATTERN)) {
			return false;
		}
		return true;
	}
	
	private boolean maybeMobilePhoneNumber(String username) {
		if (!username.matches(UserConstants.MOBILE_PHONE_NUMBER_PATTERN)) {
			return false;
		}
		return true;
	}
	
	/**
	 * 记录登录信息
	 * @param user
	 */
	public void recordLoginInfo(SysUser user) {
		user.setLoginIp(ShiroUtils.getIp());
		user.setLoginDate(DateUtils.getNowDate());
		userService.updateUserInfo(user);
	}
}

对于密码校验,可使用shiro自带的校验,在这里,博主使用自定义密码校验。

package com.layduo.framework.shiro.service;

import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.PostConstruct;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.layduo.common.constant.Constants;
import com.layduo.common.constant.ShiroConstants;
import com.layduo.common.exception.user.UserPasswordNotMatchException;
import com.layduo.common.exception.user.UserPasswordRetryLimitExceedException;
import com.layduo.common.utils.MessageUtils;
import com.layduo.framework.manager.AsyncManager;
import com.layduo.framework.manager.factory.AsyncFactory;
import com.layduo.system.domain.SysUser;

/**
 * 登录密码处理方法
 * 
 * @author layduo
 * @createTime 2019年11月8日 上午10:36:19
 */
@Component
public class SysPasswordService {

	@Autowired
	private CacheManager cacheManager;

	private Cache<String, AtomicInteger> loginRecordCache;

	@Value(value = "${user.password.maxRetryCount}")
	private String maxRetryCount;

	@PostConstruct
	public void init() {
		loginRecordCache = cacheManager.getCache(ShiroConstants.LOGINRECORDCACHE);
	}

	public void validate(SysUser user, String password) {
		String loginName = user.getLoginName();

		// 使用AtomicInteger类提供原子操作,线程安全
		AtomicInteger retryCount = loginRecordCache.get(loginName);

		// 第一次查询缓存不存在,把信息缓存
		if (retryCount == null) {
			retryCount = new AtomicInteger(0);
			loginRecordCache.put(loginName, retryCount);
		}

		// 输入密码次数超过最大值
		if (retryCount.incrementAndGet() > Integer.valueOf(maxRetryCount)) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount)));
			throw new UserPasswordRetryLimitExceedException(Integer.valueOf(maxRetryCount).intValue());
		}

		// 用户输入密码不匹配
		if (!matches(user, password)) {
			AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.count", retryCount)));
			loginRecordCache.put(loginName, retryCount);
			throw new UserPasswordNotMatchException();
		} 
		else {
			clearLoginRecordCache(loginName);
		}
	}

	/**
	 * 校验用户密码
	 * 
	 * @param user
	 * @param newPassword
	 * @return
	 */
	public boolean matches(SysUser user, String newPassword) {
		return user.getPassword().equals(encryptPassword(user.getLoginName(), newPassword, user.getSalt()));
	}

	/**
	 * 清除用户信息缓存
	 * 
	 * @param username
	 */
	public void clearLoginRecordCache(String username) {
		loginRecordCache.remove(username);
	}

	/**
	 * 对用户信息加密
	 * 
	 * @param username
	 * @param password
	 * @param salt
	 * @return
	 */
	public String encryptPassword(String username, String password, String salt) {
		return new Md5Hash(username + password + salt).toHex().toString();
	}

	public void unlock(String loginName) {
		loginRecordCache.remove(loginName);
	}
	
}

与此同时,在校验中我们采用异步方法对用户记录登录信息

package com.layduo.common.config.thread;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import com.layduo.common.utils.Threads;

/**
 * 线程池配置
 *
 * @author layduo
 **/
//@Configuration
public class ThreadPoolConfig {
	// 核心线程池大小
	private int corePoolSize = 50;

	// 最大可创建的线程数
	private int maxPoolSize = 200;

	// 队列最大长度
	private int queueCapacity = 1000;

	// 线程池维护线程所允许的空闲时间
	private int keepAliveSeconds = 300;

	@Bean(name = "threadPoolTaskExecutor")
	public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setMaxPoolSize(maxPoolSize);
		executor.setCorePoolSize(corePoolSize);
		executor.setQueueCapacity(queueCapacity);
		executor.setKeepAliveSeconds(keepAliveSeconds);
		// 线程池对拒绝任务(无线程可用)的处理策略
		executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		return executor;
	}

	/**
	 * 执行周期性或定时任务
	 */
	@Bean(name = "scheduledExecutorService")
	protected ScheduledExecutorService scheduledExecutorService() {
		return new ScheduledThreadPoolExecutor(corePoolSize,
				new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build()) {
			@Override
			protected void afterExecute(Runnable r, Throwable t) {
				super.afterExecute(r, t);
				Threads.printException(r, t);
			}
		};
	}
}
package com.layduo.framework.manager;

import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.layduo.common.utils.Threads;
import com.layduo.common.utils.spring.SpringUtils;

/**
 * 异步任务管理器
 * 
 * @author layduo
 */
public class AsyncManager {
	/**
	 * 操作延迟10毫秒
	 */
	private final int OPERATE_DELAY_TIME = 10;

	/**
	 * 异步操作任务调度线程池
	 */
	private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

	/**
	 * 单例模式
	 */
	private AsyncManager() {
	}

	private static AsyncManager me = new AsyncManager();

	public static AsyncManager me() {
		return me;
	}

	/**
	 * 执行任务
	 * 
	 * @param task
	 *            任务
	 */
	public void execute(TimerTask task) {
		executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
	}

	/**
	 * 停止任务线程池
	 */
	public void shutdown() {
		Threads.shutdownAndAwaitTermination(executor);
	}
}

 异步任务管理器调用异步工厂相关任务

package com.layduo.framework.manager.factory;
/**
 * 异步工厂 (产生任务用)
* @author layduo
* @createTime 2019年11月8日 下午2:20:04
*/

import java.util.TimerTask;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.layduo.common.constant.Constants;
import com.layduo.common.utils.AddressUtils;
import com.layduo.common.utils.ServletUtils;
import com.layduo.common.utils.spring.SpringUtils;
import com.layduo.framework.util.LogUtils;
import com.layduo.framework.util.ShiroUtils;
import com.layduo.system.domain.SysLogininfor;
import com.layduo.system.service.impl.SysLogininforServiceImpl;

import eu.bitwalker.useragentutils.UserAgent;

public class AsyncFactory {

	private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
	
	/**
	 * 记录登录信息
	 * @param username 用户名
	 * @param status 状态
	 * @param message 消息
	 * @param args 列表
	 * @return 任务task
	 */
	public static TimerTask recordLogininfor(final String username, final String status, final String message, final Object... args) {
		final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
		final String ip = ShiroUtils.getIp();
		return new TimerTask() {
			
			@Override
			public void run() {
				String address = AddressUtils.getRealAddressByIP(ip);
				StringBuilder s = new StringBuilder();
				s.append(LogUtils.getBlock(ip));
				s.append(address);
				s.append(LogUtils.getBlock(username));
				s.append(LogUtils.getBlock(status));
				s.append(LogUtils.getBlock(message));
				//打印信息到日志
				sys_user_logger.info(s.toString(), args);
				//获取客户端操作系统
				String os = userAgent.getOperatingSystem().getName();
				//获取客户端浏览器
				String browser = userAgent.getBrowser().getName();
				//封装对象
				SysLogininfor logininfor = new SysLogininfor();
				logininfor.setLoginName(username);
				logininfor.setIpaddr(ip);
				logininfor.setLoginLocation(address);
				logininfor.setBrowser(browser);
				logininfor.setOs(os);
				logininfor.setMsg(message);
				//日志状态
				if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) {
					logininfor.setStatus(Constants.SUCCESS);
				}
				else if (Constants.LOGIN_FAIL.equals(status)) {
					logininfor.setStatus(Constants.FAIL);
				}
				//插入数据
				SpringUtils.getBean(SysLogininforServiceImpl.class).insertLogininfor(logininfor);
			}
		};
	}
}

在这里,提示一下博主踩过的坑,由于分模块开发,在扫描包名的时候,我们得使用com.layduo.**.domain类似这样的写法,但因为分模块的原因,除了web模块,spring未能扫描其他模块下的domain目录,在此,我们需要添加mybatis的相关扫描配置如下:

package com.layduo.framework.config;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import javax.sql.DataSource;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

/**
 * Mybatis支持*匹配扫描包
 * 
 * @author layduo
 */
@Configuration
public class MyBatisConfig {
	@Autowired
	private Environment env;

	static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

	public static String setTypeAliasesPackage(String typeAliasesPackage) {
		ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
		MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
		List<String> allResult = new ArrayList<String>();
		try {
			for (String aliasesPackage : typeAliasesPackage.split(",")) {
				List<String> result = new ArrayList<String>();
				aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
						+ ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/"
						+ DEFAULT_RESOURCE_PATTERN;
				Resource[] resources = resolver.getResources(aliasesPackage);
				if (resources != null && resources.length > 0) {
					MetadataReader metadataReader = null;
					for (Resource resource : resources) {
						if (resource.isReadable()) {
							metadataReader = metadataReaderFactory.getMetadataReader(resource);
							try {
								result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage()
										.getName());
							} catch (ClassNotFoundException e) {
								e.printStackTrace();
							}
						}
					}
				}
				if (result.size() > 0) {
					HashSet<String> hashResult = new HashSet<String>(result);
					allResult.addAll(hashResult);
				}
			}
			if (allResult.size() > 0) {
				typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
			} else {
				throw new RuntimeException(
						"mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return typeAliasesPackage;
	}

	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
		String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
		String mapperLocations = env.getProperty("mybatis.mapperLocations");
		String configLocation = env.getProperty("mybatis.configLocation");
		typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
		VFS.addImplClass(SpringBootVFS.class);

		final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
		sessionFactory.setDataSource(dataSource);
		sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
		sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
		sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
		return sessionFactory.getObject();
	}
}

此外还有添加mapper扫描,想为了方便也可以在启动类上添加如下注解:

package com.layduo.framework.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * 程序注解配置
 *
 * @author layduo
 */
@Configuration
// 表示通过aop框架暴露该代理对象,AopContext能够访问
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要扫描的Mapper类的包的路径
@MapperScan("com.layduo.**.mapper")
public class ApplicationConfig {

}

四、测试shiro认证过程

package com.layduo.web.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.layduo.common.core.controller.BaseController;
import com.layduo.common.core.domain.AjaxResult;
import com.layduo.common.utils.ServletUtils;
import com.layduo.common.utils.StringUtils;

/**
 * 登录验证
 * 
 * @author layduo
 */
@Controller
public class SysLoginController extends BaseController {
	@GetMapping("/login")
	public String login(HttpServletRequest request, HttpServletResponse response) {
		// 如果是Ajax请求,返回Json字符串。
		if (ServletUtils.isAjaxRequest(request)) {
			return ServletUtils.renderString(response, "{\"code\":\"1\",\"msg\":\"未登录或登录超时。请重新登录\"}");
		}

		return "login";
	}

	@PostMapping("/login")
	@ResponseBody
	public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe) {
		UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
		Subject subject = SecurityUtils.getSubject();
		try {
			subject.login(token);
			return success();
		} catch (AuthenticationException e) {
			String msg = "用户或密码错误";
			if (StringUtils.isNotEmpty(e.getMessage())) {
				msg = e.getMessage();
			}
			return error(msg);
		}
	}

	@GetMapping("/unauth")
	public String unauth() {
		return "error/unauth";
	}
}

在没有进行用户信息校验成功的情况下,shiro都会将请求给设定的/login,提示用户进行用户信息认证:

 待用户进行登录信息认证通过后,shiro将跳转至设定的主页面/index

到这里,有关shiro的用户认证流程就讲完了,项目源码已上传github: https://github.com/builthuLin/layduo.git  有需要的自己fork一下~ ,想更深了解的小伙伴可百度若依框架进行自行学习和深入研究!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值