前言:前段时间在搭建公司框架安全验证的时候,就想到之前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一下~ ,想更深了解的小伙伴可百度若依框架进行自行学习和深入研究!
本文介绍了如何在SpringBoot项目中整合Shiro框架进行用户登录认证。首先,文章简述了Shiro的基本概念和优势,然后详细讲解了Shiro的内部结构和认证流程。接着,作者提供了添加Shiro依赖、配置类以及自定义UserRealm的步骤,并分享了在认证过程中遇到的问题及解决方案,如Ehcache缓存配置、分模块开发时的包扫描问题。最后,文章通过测试展示了Shiro的认证过程,并给出了项目的GitHub链接供读者参考学习。
3966

被折叠的 条评论
为什么被折叠?



