springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线

您所在的位置:网站首页 京东手机端和电脑端显示的咚咚消息一样吗 springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线

springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线

2024-07-15 03:09| 来源: 网络整理| 查看: 265

参考文章: 单点登录:https://mp.weixin.qq.com/s/DGFFPl93kZxS5G_DSFTBDA 多端登录:https://blog.csdn.net/zhourenfei17/article/details/88826911

一、前言

最近一个springboot项目要求增加一些app的功能。然后首先要改的就是这个登录的功能。我原本的登录就只是web端登录,实现了单点登录,就是同一个用户只能同时登录一次,如果再次登录的话,会清除上一次的登录信息。(具体实现参考上面的单点登录链接) 现在加了app的话,那就得实现app和web可以同时在线,如果登录了app,web就强制下线,或者登录了web,app就强制下线的,这用户体验很不好,所以得在单点登录的基础上加上多端同时在线。 区分是app登录还是pc登录,就是通过User-Agent来区分的。这个User-Agent可以和前端约定好,比如如果是移动端的登录请求的话,User-Agent的值就是mobile,如果是PC端的请求的话,User-Agent的值就是web。 在这里插入图片描述

我一开始想的是这两个终端登录的时候,分别存储不同的sessionID,比如app登录,生成的token,前缀带有mobile,web登录,生成的token前缀带有web。 在踢人的时候,根据上次登录的token,获取到前缀,去和当前登录的登录类型进行对比,如果一致说明是同一终端,就把上次的登录信息清除;如果不一致,说明不是同一终端,对上一次的登录不进行操作。 我生成sessionid用的是自定义的SessionId生成器,然后在shiroConfig里配置。

import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.session.mgt.eis.SessionIdGenerator; import java.io.Serializable; /** * 自定义SessionId生成器 */ public class ShiroSessionIdGenerator implements SessionIdGenerator { /** * 实现SessionId生成 */ @Override public Serializable generateId(Session session) { Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session); //在这里生成sessionid的时候,在"login_token_"这个token前缀前面,再加个登录类型的前缀 //比如:mobile_login_token_ 或者 web_login_token_ return String.format("login_token_%s", sessionId); } }

可是我发现,我在这里一时找不到办法来获取登录类型,那这种通过sessionid的方式来区分终端的办法就行不通。后面我找了另一种方法,就是通过定义不同登录方式的Realm来进行区分,就不对sessionid进行其他处理了。

二、整合shiro和redis 1、pom.xml引入依赖 org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-data-redis-reactive org.apache.shiro shiro-spring 1.4.0 org.crazycake shiro-redis 3.1.0 2、application.yml相关配置 spring: # 配置Redis redis: host: localhost port: 6379 timeout: 6000 #以秒为单位 password: 123456 database: 0 lettuce: pool: max-active: -1 max-wait: -1 max-idle: 16 min-idle: 8 3、新增相应的配置类,工具类

shiro授权和身份认证的话,因为要区分移动端和pc端,所以另外多加两个分别验证移动端和PC端的Realm。

(1)、ShiroRealm类

import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.HashSet; import java.util.List; import java.util.Set; /** * 统一的Shiro权限匹配和账号密码匹配 */ public class ShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private MenuService menuService; /** * 授权权限 * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principalCollection.getPrimaryPrincipal(); //这里可以进行授权和处理 Set rolesSet = new HashSet(); Set permsSet = new HashSet(); //查询角色和权限(这里根据业务自行查询) List roleList = roleService.selectRoleByUserId(user); for (Role role:roleList) { rolesSet.add(role.getRoleName()); List menuList = menuService.selectMenuByRoleId(role.getRoleId()); for (Menu menu :menuList) { permsSet.add(menu.getPerms()); } } //将查到的权限和角色分别传入authorizationInfo中 authorizationInfo.setStringPermissions(permsSet); authorizationInfo.setRoles(rolesSet); return authorizationInfo; } /** * 身份认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //因为具体的身份认证会在移动端和PC端的Realm类中实现,这里不进行处理 return null; } }

(2)、MobileShiroRealm类

import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; /** * app端登录的Realm管理 */ public class MobileShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 授权权限 * 在ShiroRealm统一处理,这里不做处理 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 身份认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //获取用户的输入的账号. String loginName = (String) authenticationToken.getPrincipal(); //通过登录名从数据库中查找 User对象,如果找到进行验证 User user = userService.getUserByName(loginName); //判断账号是否存在 if (user == null ) { throw new AuthenticationException(); } //状态为2表示已删除(或者已锁定,根据自己的用户表的实际情况而定) if ("2".equals(user.getStatus())){ throw new AuthenticationException(); } //进行验证 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户名 user.getPassword(), //密码 ByteSource.Util.bytes(user.getSalt()),//密码盐值 getName() ); //验证成功开始踢人(清除缓存和Session) ShiroUtils.deleteCache(loginName,"mobile"); return authenticationInfo; } }

(3)、WebShiroRealm类

import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; /** * web端登录的Realm管理 */ public class WebShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 授权权限 * 在ShiroRealm统一处理,这里不做处理 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 身份认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //获取用户的输入的账号. String loginName = (String) authenticationToken.getPrincipal(); //通过登录名从数据库中查找 User对象,如果找到进行验证 User user = userService.getUserByName(loginName); //判断账号是否存在 if (user == null ) { throw new AuthenticationException(); } //状态为2表示已删除 if ("2".equals(user.getStatus())){ throw new AuthenticationException(); } //进行验证 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户名 user.getPassword(), //密码 ByteSource.Util.bytes(user.getSalt()),//密码盐值 getName() ); //验证成功开始踢人(清除缓存和Session) ShiroUtils.deleteCache(loginName,"web"); return authenticationInfo; } }

(4)、ShiroModularRealmAuthenticator类

import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.realm.Realm; import java.util.Collection; import java.util.HashMap; /** * 自定义的Realm管理,主要针对多realm */ public class ShiroModularRealmAuthenticator extends ModularRealmAuthenticator { @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判断getRealms()是否返回为空 assertRealmsConfigured(); // 所有Realm Collection realms = getRealms(); // 登录类型对应的所有Realm。要注意,对应的登录类型的Realm命名,必须要包含你指定的登录类型的字符。 //比如,你指定的区分登录类型的字符串是:mobile和web,那么这两个登录的Realm类的命名,必须包含mobile或者web字符。 HashMap realmHashMap = new HashMap(realms.size()); for (Realm realm : realms) { realmHashMap.put(realm.getName(), realm); } UsernamePasswordLoginTypeToken token = (UsernamePasswordLoginTypeToken) authenticationToken; // 登录类型 String type = token.getLoginType(); if (realmHashMap.get(type) != null) { return doSingleRealmAuthentication(realmHashMap.get(type), token); } else { return doMultiRealmAuthentication(realms, token); } } }

(5)、UsernamePasswordLoginTypeToken类

import org.apache.shiro.authc.UsernamePasswordToken; /** * 重写UsernamePasswordToken方法,增加登录类型(是app端还是pc端) */ public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken { /** *登陆类型 */ private String loginType; public UsernamePasswordLoginTypeToken(String username, String password, String loginType) { super(username, password); this.loginType = loginType; } public String getLoginType() { return loginType; } public void setLoginType(String loginType) { this.loginType = loginType; } }

(6)、ShiroConfig类

import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Shiro配置类 */ @Configuration public class ShiroConfig { private final String CACHE_KEY = "shiro:cache:"; private final String SESSION_KEY = "shiro:session:"; //Redis配置 @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.password}") private String password; /** * 开启Shiro-aop注解支持 * @Attention 使用代理方式所以需要开启代码支持 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) */ /*@Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; }*/ /** * Shiro基础配置 */ @Bean public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map filterChainDefinitionMap = new LinkedHashMap(); // 注意过滤器配置顺序不能颠倒 // 配置过滤:不会被拦截的链接 filterChainDefinitionMap.put("/swagger-ui.html", "anon"); filterChainDefinitionMap.put("/swagger/**", "anon"); filterChainDefinitionMap.put("/swagger-resources/**", "anon"); filterChainDefinitionMap.put("/v2/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/uploads/**", "anon"); filterChainDefinitionMap.put("/user/login", "anon"); filterChainDefinitionMap.put("/user/unauth", "anon"); filterChainDefinitionMap.put("/**", "authc"); // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 shiroFilterFactoryBean.setLoginUrl("/user/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 安全管理器 */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setAuthenticator(modularRealmAuthenticator()); // 自定义Ssession管理 securityManager.setSessionManager(sessionManager()); // 自定义Cache实现 securityManager.setCacheManager(cacheManager()); List realms = new ArrayList(); // 统一角色权限控制realm realms.add(shiroRealm()); // app登录realm realms.add(mobileShiroRealm()); // web登录realm realms.add(webShiroRealm()); securityManager.setRealms(realms); return securityManager; } /** * 自定义的Realm管理,主要针对多realm */ @Bean("myModularRealmAuthenticator") public ShiroModularRealmAuthenticator modularRealmAuthenticator() { ShiroModularRealmAuthenticator customizedModularRealmAuthenticator = new ShiroModularRealmAuthenticator(); // 设置realm判断条件 customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return customizedModularRealmAuthenticator; } /** * 统一的身份验证器 */ @Bean public ShiroRealm shiroRealm() { ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setName("web"); return shiroRealm; } /** * app端的身份验证器 */ @Bean public MobileShiroRealm mobileShiroRealm() { MobileShiroRealm shiroRealm = new MobileShiroRealm(); shiroRealm.setName(UserConstant.APP); //自定义的密码验证器 shiroRealm.setCredentialsMatcher(shiroRetryLimit(cacheManager())); return shiroRealm; } /** * web端的身份验证器 */ @Bean public WebShiroRealm webShiroRealm() { WebShiroRealm shiroRealm = new WebShiroRealm(); shiroRealm.setName(UserConstant.WEB); //自定义的密码验证器 shiroRealm.setCredentialsMatcher(shiroRetryLimit(cacheManager())); return shiroRealm; } /** * 配置Redis管理器 * @Attention 使用的是shiro-redis开源插件 */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setTimeout(timeout); redisManager.setPassword(password); return redisManager; } /** * 配置Cache管理器 * 用于往Redis存储权限和角色标识 * @Attention 使用的是shiro-redis开源插件 */ @Bean("ehCacheManager") public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setKeyPrefix(CACHE_KEY); // 配置缓存的话要求放在session里面的实体类必须有个id标识 redisCacheManager.setPrincipalIdFieldName("userId"); return redisCacheManager; } /** * 自定义的SessionID生成器 */ @Bean public ShiroSessionIdGenerator sessionIdGenerator(){ return new ShiroSessionIdGenerator(); } /** * 配置RedisSessionDAO * @Attention 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setSessionIdGenerator(sessionIdGenerator()); redisSessionDAO.setKeyPrefix(SESSION_KEY); redisSessionDAO.setExpire(timeout); return redisSessionDAO; } /** * 配置Session管理器 */ @Bean public SessionManager sessionManager() { //自定义的Session管理器,获取token ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); shiroSessionManager.setSessionDAO(redisSessionDAO()); return shiroSessionManager; } /** * 自定义的密码验证器 */ @Bean("shiroRetryLimit") public ShiroRetryLimit shiroRetryLimit(CacheManager cacheManager){ ShiroRetryLimit shiroRetryLimit = new ShiroRetryLimit(cacheManager); // 散列算法:这里使用SHA256算法; shiroRetryLimit.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME); // 散列的次数,比如散列两次,相当于 md5(md5("")); shiroRetryLimit.setHashIterations(SHA256Util.HASH_ITERATIONS); return shiroRetryLimit; } }

(7)、ShiroUtils类

import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.Authenticator; import org.apache.shiro.authc.LogoutAware; import org.apache.shiro.session.Session; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.support.DefaultSubjectContext; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.crazycake.shiro.RedisSessionDAO; import java.util.Collection; import java.util.List; import java.util.Objects; /** * Shiro工具类 */ public class ShiroUtils { /** 私有构造器 **/ private ShiroUtils(){ } private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class); private static RoleService roleService = SpringUtil.getBean(RoleService.class); /** * 获取当前用户Session */ public static Session getSession() { return SecurityUtils.getSubject().getSession(); } /** * 用户退出登录 */ public static void logout() { SecurityUtils.getSubject().logout(); } /** * 获取当前用户信息 */ public static User getUserInfo() { return (User) SecurityUtils.getSubject().getPrincipal(); } /** * 获取当前用户id */ public static Integer getUserId() { return getUserInfo().getUserId(); } /** * 删除用户缓存信息 * @Param loginName 用户登录名称 * @Param loginType 当前登录的登录方式(mobile 移动端登录,web PC端登录) */ public static void deleteCache(String loginName, String loginType){ //从缓存中获取Session Session session = null; Collection sessions = redisSessionDAO.getActiveSessions(); User sysUserEntity; Object attribute = null; for(Session sessionInfo : sessions){ //遍历Session,找到该用户名称对应的Session attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (attribute == null) { continue; } System.out.println(((SimplePrincipalCollection) attribute).getPrimaryPrincipal()); sysUserEntity = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal(); if (sysUserEntity == null) { continue; } if (Objects.equals(sysUserEntity.getUserName(), loginName)) { session=sessionInfo; break; } } if (session == null||attribute == null) { return; } /** * 根据当前登录类型,在attribute中查找上一次登录的Realm, * 1、如果有值,说明当前登录的终端和上一次登录的终端相同,则删除上一次登录的session(清空上一次的登录信息,重新登录)。 * 2、如果没有值,说明当前登录的终端和上一次登录的终端不一样,不对上一次登录的session删除,实现app端和web端可同时在线 */ Collection collection = ((SimplePrincipalCollection) attribute).fromRealm(loginType); if (collection.size()>0){ //删除session redisSessionDAO.delete(session); } //删除Cache,在访问受限接口时会重新授权 DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); Authenticator authc = securityManager.getAuthenticator(); ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute); } } 4、controller类

在controller层中,通过请求头的User-Agent 来区分登录终端是移动端还是PC端,这个User-Agent可以和前端约定好,比如如果是移动端的登录请求的话,User-Agent的值就是mobile,如果是PC端的请求的话,User-Agent的值就是web。还有其他登录方式,根据自己的实际情况而定。

import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.AllArgsConstructor; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authz.annotation.RequiresUser; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.*; /** * 登录 */ @RestController @RequestMapping("/user") public class LoginController { /** * 登录 */ @RequestMapping("/login") public ResultVo login(@RequestBody User user, HttpServletRequest request){ Map map = new HashMap(); //进行身份验证 try{ //验证身份和登陆 Subject subject = SecurityUtils.getSubject(); //获取登录的终端 String loginType = request.getHeader("User-Agent"); UsernamePasswordLoginTypeToken token = new UsernamePasswordLoginTypeToken(user.getUserName(), user.getPassword(), loginType); //进行登录操作 subject.login(token); }catch (IncorrectCredentialsException e) { return ResultVoUtil.error(1000,e.getMessage()); } catch (LockedAccountException e) { return ResultVoUtil.error(1004,e.getMessage()); } catch (AuthenticationException e) { return ResultVoUtil.error(ResultEnum.USER_NOT_ERROR); } catch (Exception e) { return ResultVoUtil.error(ResultEnum.UNKNOWN_EXCEPTION); } map.put("code",0); map.put("msg","登录成功"); map.put("token", ShiroUtils.getSession().getId().toString()); return ResultVoUtil.success(map); } /** * 未登录 */ @ApiOperation("未登录") @RequestMapping("/unauth") public ResultVo unauth(){ return ResultVoUtil.error(ResultEnum.USER_NOT_LOGIN); } /** * 登出 */ @ApiOperation("退出登录") @RequestMapping("/logout") @RequiresUser @Log(operMethod = "退出登录",operInfo = "退出登录") public ResultVo logout(){ //登出Shiro会帮我们清理掉Session和Cache ShiroUtils.logout(); return ResultVoUtil.success(0,"退出登录成功"); } }

文中还用到的一些其他配置类和工具类,完整的可以看我开头放的单点登录的那篇链接,这里我是在这篇链接上的单点登录的基础上,再加了移动端和PC端同时在线:如果登录终端是同一个的话,就会踢掉上一次的登录,不同的话,则多个终端可同时在线。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3