1、权限控制的两种方式:粗粒度基于URL级别权限控制、细粒度基于方法级别权限控制
2、基于Apache Shiro实现登录认证和权限控制,重点讲解shiro权限控制流程、自定义Realm对象控制系统认证和授权
3、Apache Shiro实现细粒度方法级别权限控制
4、动态系统菜单显示功能
5、对认证和授权数据进行缓存优化
用户发送请求,访问页面,通过配置的Filter过滤器进行拦截,判断当前用户是否具有访问页面(Url)的权限(根据shiro配置,那些页面/资源需要进行认证,那些页面/资源可以直接访问),如果有放行,如果没有提示权限不足,禁止访问
可以基于 Filter 实现(基于页面访问路径)在数据库中存放 用户、权限、访问 URL 对应关系, 当前用户访问一个 URL 地址,查询数据库判断用户当前具有权限,是否包含这个 URL,如果包含允许访问,如果不包含 权限不足 !!!
用户发送请求,访问方法,在方法的Service(业务逻辑层),通过注解方式,判断此用户是否具有执行方法的权限,有就放行,没有进行拦截
可以代理、自定义注解实现,访问目标对象方法,在方法上添加权限注解信息,对目标对象创建代理对象,访问真实对象先访问代理对象,在代理对象查询数据库判断是否具有注解上描述需要权限,具有权限允许访问,不具有权限,拦截访问,提示权限不足
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
1) Authentication 认证: 用户认证,身份识别 (你是谁?)
2) Authorization 授权:用户具有哪些权限、角色 (你能做什么?)
3) Cryptography安全数据加密
4) Session Management 会话管理
5) Web Integration web系统集成
6) Integrations 集成其他应用,spring、缓存框架
1、核心介绍
1)Application Code用户编写代码
2)Subject就是shiro管理的用户(相当于登录的用户对象)
3)SecurityManager安全管理器,就是shiro权限控制核心对象,在编程时,只需要操作Subject方法,底层调用 SecurityManager方法,无需直接编程操作SecurityManager
4)Realm应用程序和安全数据之间连接器,应用程序进行权限控制读取安全数据(数据表、文件、网络…)通过Realm对象完 成
2、Shiro执行流程
应用程序—>Subject—>SecurityManager—>Realm—>安全数据
3、Shiro进行权限控制的四种主要方式
1)在程序中通过Subject编程方式进行权限控制
2)配置Filter实现URL级别粗粒度权限控制
3)配置代理,基于注解实现细粒度权限控制
4)在页面中使用shiro自定义标签实现,页面显示权限控制
org.apache.shiro
shiro-spring
1.3.2
org.apache.shiro
shiro-ehcache
1.4.0
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
@Configuration
public class ShiroConfig {
@Bean // shiroFilter 过滤器
public ShiroFilterFactoryBean shirFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
System.out.println("====ShiroConfiguration.shirFilter()====");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
shiroFilterFactoryBean.setLoginUrl("/");// 设置首页
// 登录成功后要跳转的链接
// shiroFilterFactoryBean.setSuccessUrl("/login/home");
// // 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/shiroError");
// 添加shiro内置过滤器 filterChainDefinitionMap
/*
* anon:表示可以匿名使用。 authc:表示需要认证(登录)才能使用,没有参数
* roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[
* "admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
* perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[
* "user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* rest:根据请求的方法,相当于/admins/user/**=perms[user:method]
* ,其中method为post,get,delete等。
* port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,
* 其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,
* queryString是你访问的url里的?后面的参数。 authcBasic:没有参数表示httpBasic认证
* ssl:表示安全的url请求,协议为https user:当登入操作时不做检查
*/
Map filterChainDefinitionMap = new LinkedHashMap();
// // 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/", "anon"); // 可以匿名访问"/"方法
filterChainDefinitionMap.put("/kickout", "anon"); //踢出用户方法,跳转到登录页
filterChainDefinitionMap.put("/add", "authc"); // 必须认证才能访问 测试用
filterChainDefinitionMap.put("/update", "authc"); // 必须认证才能访问 测试用
filterChainDefinitionMap.put("/tologin", "anon");// 可以匿名访问"/tologin"方法
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/image/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
//
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout"); // 退出登录方法
// :这是一个坑呢,一不小心代码就不好使了;
//user 验证通过或RememberMe登录的都可以 kickout 自定义过滤器(需要验证并发登录,踢出重复登录)
filterChainDefinitionMap.put("/**", "user,kickout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//自定义拦截器 key:名称 value:filter(过滤器)
Map filtersMap = new LinkedHashMap();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
//设置自定义过滤器到shiro过滤器工厂
shiroFilterFactoryBean.setFilters(filtersMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 自定义retryLimitCredentialsMatcher的作用是为了设置一个和密码加密算法一样的算法,替代了凭证管理器HashedCredentialsMatcher
* 在注册用户时会根据设置的加密方式和次数对明文密码加密保存
* 在登录的时候会根据用户密码加密匹配DB密码,同时记录登录次数,超过限制次数限制登录时间
*/
@Bean
public RetryLimitCredentialsMatcher getRetryLimit(){
//构造方法实例化RetryLimitCredentialsMatcher对象,从指定cache缓存中获取缓存信息
RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(cacheManager());
//设置缓存加密方式 默认MD5
retryLimitCredentialsMatcher.setHashAlgorithmName("md5");
//设置缓存加密次数
retryLimitCredentialsMatcher.setHashIterations(2);
retryLimitCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return retryLimitCredentialsMatcher;
}
@Bean // 自定义Realm
public ShiroRealm myShiroRealm() {
//自定义Realm
ShiroRealm myShiroRealm = new ShiroRealm();
//设置MD5加密规则 在Realm登录认证的时候会加密用户输入的密码,是否和保存DB的密码一致
// myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
//设置加密规则 在Realm登录认证的时候会加密用户输入的密码,是否和保存DB的密码一致,同时记录登录次数,限制多次登录
myShiroRealm.setCredentialsMatcher(getRetryLimit());
return myShiroRealm;
}
/**
* cookie对象; rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
*
* @return
*/
@Bean
public SimpleCookie rememberMeCookie() {
// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setSecure(true);
// 只能通过httpservlet访问,无法通过js脚本将无法读取到cookie信息
simpleCookie.setHttpOnly(true);
//
simpleCookie.setMaxAge(60 * 10);
return simpleCookie;
}
// 记住我的配置
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCookie(rememberMeCookie());
rememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return rememberMeManager;
}
/**
* 自定义shiroSession的cookie,如果不设置默认JSESSIONID,会与tomcat等默认cookie名重复,sessionIdCookie用于保存sessionId标识
*
* @return
*/
@Bean
public SimpleCookie sessionIdCookie() {
// 这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("sid");
// setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
// 设为true后,只能通过http访问,javascript无法访问
// 防止xss读取cookie
simpleCookie.setHttpOnly(true);
// simpleCookie.setPath("/");
// maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
/**
* 配置session监听
* shiroSession时间SessionListener监听,原来的HttpSessionListener接口监听Session的创建和失效
*
* @return
*/
@Bean("sessionListener")
public ShiroSessionListener sessionListener() {
ShiroSessionListener sessionListener = new ShiroSessionListener();
return sessionListener;
}
/**
* 配置会话ID生成器
*
* @return
*/
@Bean
public SessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
// shiro 缓存
@Bean
public EhCacheManager cacheManager() {
EhCacheManager cache = new EhCacheManager();
// 设置ehcache缓存的配置文件
cache.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cache;
}
/**
* SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 MemorySessionDAO
* 直接在内存中进行会话维护 EnterpriseCacheSessionDAO
* 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
* CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager
*
* @return
*/
@Bean
public SessionDAO sessionDAO() {
EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
// 使用ehCacheManager 设置缓存配置
enterpriseCacheSessionDAO.setCacheManager(cacheManager());
// 设置session缓存的名字
// 默认为shiro-activeSessionCache,在cacheManager缓存配置xml中name已经定义了
// enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
// sessionId生成器 生成的sessionId
enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
return enterpriseCacheSessionDAO;
}
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话
sessionManager.setSessionValidationSchedulerEnabled(true); // session可以使用该定时调度器进行检测
sessionManager.setSessionValidationInterval(10000); // 多长时间检查一次session有效性
// 会话超时时间,单位:毫秒
sessionManager.setGlobalSessionTimeout(180000); // 超时时间3600秒
// 去掉shiro登录时url里的JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
// 开启自定义cookie 指定sessionid
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(sessionIdCookie());
Collection listeners = new ArrayList();
// 配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
// 设置session缓存持久化到内存
sessionManager.setSessionDAO(sessionDAO());
// Shiro提供SessionDAO用于会话的CRUD
// sessionManager.setCacheManager(cacheManager());
return sessionManager;
}
@Bean // 安全管理器
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm. 全局安全管理期设置自定义realm
securityManager.setRealm(myShiroRealm());
// 配置自定义session管理 全局安全管理期设置session管理
securityManager.setSessionManager(sessionManager());
// 配置记住我 全局安全管理器设置记住我
securityManager.setRememberMeManager(rememberMeManager());
// 全局安全管理器设置缓存
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/**
* 限制同一账号登录同时登录人数控制
* @return 过滤器
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
//这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
//也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
kickoutSessionControlFilter.setCacheManager(cacheManager());
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/kickout");
return kickoutSessionControlFilter;
}
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启aop注解支持,解决spingboot环境中使用Shiro注解(如@RequiresRoles,@RequiresPermissions)无效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
sourceAdvisor.setSecurityManager(securityManager);
return sourceAdvisor;
}
/**
* thymeleaf模板使用shiro注解
* @return
*/
@Bean
public ShiroDialect shiroDialect() { // thymeleaf模板使用shiro注解
return new ShiroDialect();
}
}
判断登录用户是否重复登录,踢出最早登录的
/**
* shiro强大的自定义访问控制拦截器:AccessControlFilter
* isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
*
* onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
*
* onPreHandle:会自动调用这两个方法决定是否继续处理;
*
*/
public class KickoutSessionControlFilter extends AccessControlFilter {
private static final Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession = 1;
private String kickoutAttrName = "kickout";
private SessionManager sessionManager;
private Cache> cache;
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* 设置Cache的key的前缀 shiro-kickout-session cache配置
*/
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-kickout-session");
}
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
Subject subject = getSubject(request, response);
//isAuthenticated 成功登录 isRemembered 被记忆的
if(!subject.isAuthenticated() && !subject.isRemembered())
{
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
User user = (User) subject.getPrincipal();
String username = user.getUserName();
Serializable sessionId = session.getId();
logger.info("进入KickoutControl, sessionId:{}", sessionId);
//读取缓存 根据用户名 查询缓存cache对象 获取value(缓存的sessionId) 没有就存入
Deque deque = cache.get(username);
if(deque == null) {
deque = new LinkedList();
cache.put(username, deque);
}
//如果队列里没有此sessionId,且用户没有被踢出;放入队列deque
if(!deque.contains(sessionId) && session.getAttribute(kickoutAttrName) == null) {
//将sessionId存入队列
deque.push(sessionId);
}
logger.info("缓存登录用户的SessionId--deque.size:{}",deque.size());
//如果队列里的sessionId数超出最大会话数,开始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) {
//如果踢出后者
kickoutSessionId = deque.removeFirst();
} else {
//否则踢出前者
kickoutSessionId = deque.removeLast();
}
//踢出后再更新下缓存队列
cache.put(username, deque);
try {
//获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute(kickoutAttrName, true);
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute(kickoutAttrName) != null && (Boolean)session.getAttribute(kickoutAttrName) == true) {
//会话被踢出了
try {
//退出登录
subject.logout();
} catch (Exception e) {
logger.warn(e.getMessage());
e.printStackTrace();
}
saveRequest(request);
//重定向
logger.info("用户登录人数超过限制, 重定向到{}", kickoutUrl);
String reason = URLEncoder.encode("账户已超过登录人数限制", "UTF-8");
String redirectUrl = kickoutUrl + (kickoutUrl.contains("?") ? "&" : "?") + "shiroLoginFailure=" + reason;
WebUtils.issueRedirect(request, response, redirectUrl);
return false;
}
return true;
}
}
实现明文密码加密,登录密码匹配,限制登录尝试次数,5次后限制10分钟登录
**
* @author 张江丰
* 密码验证+登录次数限制
*/
@Component
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
/**
* 密码输入错误次数就被冻结
*/
private Integer errorPasswordTimes=5;
/**
* 缓存对象Cache 根据缓存配置名称:passwordRetryCache 获取指定的缓存信息
*/
private Cache passwordRetryCache;
private static final Log logger = LogFactory.getLog(RetryLimitCredentialsMatcher.class);
/**
* 构造方法 创建对象,传入缓存的管理器
* @param cacheManager
*/
public RetryLimitCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
/**
* 方法名: doCredentialsMatch
* 方法描述: 用户登录错误次数方法.
* @param token 登录用户信息对象
* @param info shiro认证用户信息对象
* @return boolean
* @throws
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token,
AuthenticationInfo info) {
logger.info("密码登录次数验证执行了......");
//获取登录用户名
String username = (String) token.getPrincipal();
//passwordRetryCache(ehcache缓存对象),获取所有keys(缓存的用户)的set集合
// Set keys = passwordRetryCache.keys();
//AtomicInteger 高并发下使用的线程安全的int类 从passwordRetryCache缓存map结构中 根据key(登录用户名)获取value(登录次数)
AtomicInteger retryCount = passwordRetryCache.get(username);
//retryCount=null 第一次登录 设置登录对象的缓存信息
if (retryCount == null) {
retryCount = new AtomicInteger(0);
//设置缓存信息 用户名:key 登录次数:value
passwordRetryCache.put(username, retryCount);
}
//retryCount.incrementAndGet()方法,每次判断都会把用户的登录次数缓存+1,如果登录成功会清空缓存信息
//如果登录次数>5次,抛出异常在登录的controller拦截此异常处理
if (retryCount.incrementAndGet() > errorPasswordTimes) {
// if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
//匹配token(登录用户信息)和info(shiro认证信息),这里token密码加盐与info比较
boolean matches = super.doCredentialsMatch(token,info);
if (matches) {
// clear retry count 如果信息匹配上(登录成功) 删除缓存里的用户信息
passwordRetryCache.remove(username);
}
return matches;
}
}
public class ShiroRealm extends AuthorizingRealm {
private static final Log logger = LogFactory.getLog(ShiroRealm.class);
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionsService permissionsService;
/**
* @author 张江丰 授权 完成登录认证后会执行授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("shiro 授权管理");
// 授权管理对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 查询出用户
User user = (User) principals.getPrimaryPrincipal();
if (user.getUserName().equals("admin")) {
// 设置超级管理员
info.addRole("admin");
info.addStringPermission("admin");
} else {
// List pers = null;
// 查询用户-->角色
List roles = roleService.userByRole(user.getId());
// info.addRoles((Collection) roles);
for (Role role : roles) {
// 查询角色对应的权限
info.addRole(role.getDescr());
// pers.addAll(permissionsService.userBypermisson(role.getId()));
List userBypermisson = permissionsService.userBypermisson(role.getId());
for (Permissions permissions : userBypermisson) {
info.addStringPermission(permissions.getDescr());
}
}
}
// return null;
return info;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("shiro 认证管理");
// 获取用户登录信息
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
User user = userService.findByUsername(usernamePasswordToken.getUsername());
if (user == null) {
// 用户名不存在 抛出指定异常,跳转到登录页面
return null;
} else {
setSession("user", user);
//获取盐值 与用户输入密码进行加密是否匹配DB密码 ps:salt盐值,是新增用户时随机创建并保存的数据
ByteSource salt = ByteSource.Util.bytes(user.getSalt());
//身份认证返回对象 参数:s1 用户对象 s2:用户密码 s3:加密盐值 s4:realm名称
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
return simpleAuthenticationInfo;
}
}
/**
* 将一些数据放到ShiroSession中,以便于其它地方使用,将用户存放到session中
*
* @see // 比如Controller,使用时直接用HttpSession.getAttribute(key)就可以取到
*/
private void setSession(Object key, Object value) {
Subject currentUser = SecurityUtils.getSubject();
if (currentUser != null) {
Session session = currentUser.getSession();
System.out.println("==========" + session.getId() + "=============");
if (null != session) {
// 2小时
// session.setTimeout(60 * 10);// 3600秒超时
session.setAttribute(key, value);
}
}
}
}
项目启动,用户访问创建session,就会触发自定义ShiroSessionListener,分别拦截session创建,退出,销毁方法
/**
* shiroSession时间SessionListener监听,原来的HttpSessionListener接口监听Session的创建和失效
*
* @author 张江丰
*
*/
public class ShiroSessionListener implements SessionListener {
private static final Log logger = LogFactory.getLog(ShiroSessionListener.class);
/**
* 统计在线人数 juc包下线程安全自增
*/
private final AtomicInteger sessionCount = new AtomicInteger(0);
/**
* 会话创建时触发
*
* @param session
*/
@Override
public void onStart(Session session) {
// 会话创建,在线人数加一
sessionCount.incrementAndGet();
logger.info("=============shiroSession会话创建成功" + session.getId() + "=================");
}
/**
* 退出会话时触发
*
* @param session
*/
@Override
public void onStop(Session session) {
// 会话退出,在线人数减一
sessionCount.decrementAndGet();
logger.info("==============shiroSession会话退出" + session.getId() + "================");
}
/**
* 会话过期时触发
*
* @param session
*/
@Override
public void onExpiration(Session session) {
// 会话过期,在线人数减一
sessionCount.decrementAndGet();
logger.info("==============shiroSession会话过期" + session.getId() + "================");
}
/**
* 获取在线人数使用
*
* @return
*/
public AtomicInteger getSessionCount() {
return sessionCount;
}
}
/**
* shiro 全局异常处理 Controller继承BaseController公共异常类,在抛出异常时会拦截异常处理器
*
* @ControllerAdvice aop切面技术,此注解相当于声明了切面 basePackages = "com.supplier.utils"相当于切点指定捕获异常的类(可以省略,spring自动扫描识别) @ExceptionHandler相当于通知 具体方法
* @ExceptionHandler 全局异常处理器 全局异常捕获,捕获规则 s1:根据异常继承的层级关系优先捕获被父类捕获) s2:子类抛异常只能小于或等于父类的异常
* 登录认证异常:UnauthenticatedException,AuthenticationException
* 权限认证异常:UnauthorizedException,AuthorizationException
*
* @author 张江丰
*
*/
@ControllerAdvice(basePackages = "com.supplier.utils")
public class BaseController {
/**
* 登录认证异常
*
* @param request
* @param response
* @return
*/
// @ExceptionHandler({ UnauthenticatedException.class,
// AuthenticationException.class })
// public String authenticationException(HttpServletRequest request,
// HttpServletResponse response) {
// Map map = new HashMap<>();
// map.put("status", "-1000");
// map.put("message", "未登录");
// writeJson(map, response);
// return null;
// }
/**
* 权限异常
*
* @ExceptionHandler 全局异常捕获,捕获规则(根据异常继承的层级关系优先捕获被父类捕获),子类抛异常只能小于或等于父类的异常
* UnauthorizedException 父类拦截未授权异常
* AuthorizationException 父类拦截授权异常(不匹配)
* @return
*/
@ExceptionHandler({ UnauthorizedException.class, AuthorizationException.class })
public void authorizationException(HttpServletRequest request, HttpServletResponse response) {
Map map = new HashMap<>();
map.put("code", "-1001");
map.put("msg", "无操作权限");
writeJson(map, response);
}
private void writeJson(Map map, HttpServletResponse response) {
// 字符打印流
PrintWriter out = null;
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
out = response.getWriter();
JSONObject mapJson = JSONObject.fromObject(map);
String string = mapJson.toString();
out.write(string);
} catch (IOException e) {
} finally {
if (out != null) {
out.close();
}
}
}
}
/**
* @author 张江丰
*/
@Controller
public class UserController extends BaseController {
private static final Log logger = LogFactory.getLog(UserController.class);
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
// @Autowired
// private CookieUtil cookieUtil;
@RequestMapping("/")
public String hello() {
logger.info("-----------首页入口执行-----------");
return "login";
}
@RequestMapping("/kickout")
public String kickout() {
logger.info("-----------踢出重复登录用户-----------");
return "login";
};
/**
* shiro 权限注解 value可以多值通过","分开,logical表示权限是并且和或的关系
新增用户
*
* @return
*/
@RequiresPermissions(value = {"admin"}, logical = Logical.OR)
@RequestMapping("/add")
@ResponseBody
String userName = request.getParameter("userName");
String password = request.getParameter("password");
//随机生成盐值 salt
String salt = getRandomString();
//原生密码混合salt
// password=salt.charAt(1)+salt.charAt(3)+password+salt.charAt(5)+salt.charAt(6);
//调用封装shiro的MD5加密方法(s1:密码,s2:随机盐值,s3:加密次数)
String Md5Password = getPassword(password, salt, 2);
User user = new User();
user.setUserName(userName);
user.setPassword(Md5Password);
user.setPhone("15629048422");
user.setIdNumber("420116199308160074");
user.setCreateTime(new Date());
user.setSalt(salt);
user.setNickName("汪汪汪");
user.setState("0");
user.setGroupId(1);
user.setSuperMan("0");
user.setParentId(1);
userService.add(user);
return "新增方法";
}
/**
* 密码加密方法
* @param password 密码
* @param salt 盐值
* @param hashTimes 加密次数
* @return
*/
public static String getPassword(String password,String salt,int hashTimes){
// SimpleHash md5Hsh = new SimpleHash(password,salt,hashTimes);
Md5Hash md5Hash = new Md5Hash(password,salt,hashTimes);
return md5Hash.toString();
}
/**
* 盐值字符串
*/
String range;
{
range = "0123456789abcdefghijklmnopqrstuvwxyz";
}
/**
* 获取随机盐值
* @return
*/
public String getRandomString() {
Random random = new Random();
StringBuffer result = new StringBuffer();
//要生成几位,就把这里的数字改成几
for (int i = 0; i < 7; i++) {
result.append(range.charAt(random.nextInt(range.length())));
}
return result.toString();
}
//登录
@RequestMapping("/tologin")
@ResponseBody
public CommonResponse login(HttpServletRequest request, HttpServletResponse response,
Map paramMap) {
String username = request.getParameter("userName");
String password = request.getParameter("password");
boolean rememberMe = request.getParameter("rememberMe") != null;
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// RememberMe这个参数设置为true后,在登陆的时候就会在客户端设置remenberme的相应cookie。
// 下次访问带上这个cookie,访问链接为user链接器的,就不需要进行登录验证,直接进入权限验证。
if (rememberMe) {
token.setRememberMe(true);
}
Subject subject = SecurityUtils.getSubject();
String error = null;
User user = null;
try {
subject.login(token);
//这里的catch到的异常会被继承的父类BaseController处理
} catch (UnknownAccountException | IncorrectCredentialsException e) {
error = "用户名或密码错误";
}catch (ExcessiveAttemptsException e){
error ="登录错误次数超过五次,请十分钟后登录!";
} catch (AuthenticationException e) {
error = "其它错误:" + e.getMessage();
}
logger.error("错误信息:" + error);
//获取shiro保存的用户
user = (User) subject.getPrincipal();
if (error != null) {
paramMap.put("error", error);
return CommonResponseUtil.success(paramMap);
// return "login";
} else {
paramMap.put("user", user.getNickName());
return CommonResponseUtil.success(paramMap);
// return "admin/index";
}
}
@RequestMapping("/logout")
@ResponseBody
public CommonResponse logout(){
logger.info("执行shiro的logout退出登录操作!");
Subject subject = SecurityUtils.getSubject();
subject.logout();
return CommonResponseUtil.success("退出成功!");
}
}