springboot+shiro自定义拦截器互踢问题

shiro自定义拦截器继承AccessControllerFilter,实现session互踢机制。
应用场景:
我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。
分析:
spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这2个方法:isAccessAllowed、onAccessDenied
isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
部分代码:

public class KickOutSessionControlFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory.getLogger(KickOutSessionControlFilter.class);

    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickOutAfter = false;
    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;
    /**
     * 会话管理器
     */
    private SessionManager sessionManager;
    /**
     * 会话缓存
     */
    private Cache<String, Deque<Serializable>> cache;

    public void setKickOutAfter(boolean kickOutAfter) {
        this.kickOutAfter = kickOutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public void setCacheManager(RedisCacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    /**
     * 是否允许访问,返回true表示允许
     *
     * @param servletRequest
     * @param servletResponse
     * @param obj
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object obj) {
        return false;
    }

    /**
     * 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 1同一个用户在不同ip上,不可以同时访问,后者会把前者踢出,即同一个用户不可以同时访问
        Subject subject = getSubject(servletRequest, servletResponse);
        System.out.println("===当前subject:==" + SecurityUtils.getSubject());
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            // 如果没有登录,直接进行之后的拦截器链
            return true;
        }
        // 当前用户
        User user = (User) subject.getPrincipal();
        String username = user.getUserName();
        
        // 当前会话
        Session session = subject.getSession();
        Serializable sessionId = session.getId();
        
        // 读取缓存用户 没有就存入
        Deque<Serializable> deque = cache.get(username);
        if (deque == null) {
            // 初始化队列
            deque = new ArrayDeque<Serializable>();
        }
        
        // 如果队列里没有当前会话sessionId,且当前会话未设置踢出标记(用户没有被踢出),放入队列
        if (!deque.contains(sessionId) && session.getAttribute("kickOut") == null) {
            // 将用户的sessionId存入队列
            deque.push(sessionId);
            // 将用户的sessionId存入队列缓存
            cache.put(username, deque);
        }
        
        // 如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickOutSessionId = null;
            // 是否踢出后来登录的,默认是false,即后者登录的用户踢出前者登录的用户;
            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("kickOut", true);
                    System.out.println("===将sessionId:==" + kickOutSession.getId() + "设置踢出标记");
                }
            } catch (Exception e) {
                // ignore exception
            }
        }
        // ajax请求,如果被踢出了,(前者或后者)直接退出,返回相应的状态
        if (session.getAttribute("kickOut") != null && (Boolean) session.getAttribute("kickOut") == true) {
            // 当前会话踢出标记不为空且等于true,会话被踢出了
            try {
                // 退出登录
                String ip = IPUtil.getIpAddress((HttpServletRequest) servletRequest);
                String url = ((HttpServletRequest) servletRequest).getRequestURL() + "";
                SecurityLogoutFilter.logout(subject);
                logger.info("IP地址为:" + ip + "的用户【" + username + "】被踢出,已在其他ip地址登录");
                ResponseUtil.returnResultAjax();
                return false;
            } catch (Exception e) {
                // ignore
            }
            return false;
        }
        return true;
    }
}

shiroFilterFactoryBean方法

		// 自定义过滤器
        Map<String, Filter> filters = new HashMap<>();
        // 同一用户登陆互踢
        filters.put("kickOut", kickOutSessionControlFilter());
		filterChainDefinitionMap.put("/**", "kickOut,authc");

定义拦截器的时候不需要加@Bean;

	//@Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        //用于根据会话ID,获取会话进行踢出操作的;
        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址
        kickoutSessionControlFilter.setKickoutUrl("/a/login");
        return kickoutSessionControlFilter;
    }

互踢分析:

1A用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
2.1A用户第一次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量未超过1,当前会话的标记为空进入后面的拦截器
2.11A用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
2.2B用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器未拦截住,进行正常登录获得会话token
2.21B用户第一次访问请求,进入互踢过滤器,获取当前会话,从cache中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中A的token删除,cache更新,将A的会话设置标记,当前会话的标记为空进入后面的拦截器
2.22B用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
3.1A用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回
3.2A用户第四次访问请求,会话过期请重新登录
4.1A用户第二次访问登录,进入进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
4.2A用户第四次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中B的token删除,cache更新,将B的会话设备标记,当前会话的标记为空进入后面的拦截器
5B用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的demo项目,基于Spring Boot集成Shiro和JWT进行权限控制,使用MyBatis操作数据库。 1. 创建一个Spring Boot项目,引入相关依赖: ``` <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> ``` 2. 配置Shiro相关的Bean和JWT相关的Bean,创建一个ShiroConfig类,并添加以下内容: ``` @Configuration public class ShiroConfig { @Bean public Realm realm() { // 创建自定义的Realm return new MyRealm(); } @Bean public SecurityManager securityManager() { // 创建SecurityManager,并设置Realm DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm()); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilter() { // 创建ShiroFilterFactoryBean,并设置SecurityManager和拦截器 ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager()); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**", "jwt"); shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilter; } @Bean public JwtRealm jwtRealm() { // 创建JwtRealm return new JwtRealm(); } @Bean public JwtFilter jwtFilter() { // 创建JwtFilter return new JwtFilter(); } } ``` 3. 配置MyBatis相关的Bean,创建一个MyBatisConfig类,并添加以下内容: ``` @Configuration @MapperScan("com.example.demo.mapper") public class MyBatisConfig { @Bean public DataSource dataSource() { // 创建H2数据库 return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("schema.sql") .build(); } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { // 创建SqlSessionFactory,并设置DataSource和Mapper SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource()); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sessionFactory.setMapperLocations(resolver.getResources("classpath:/mapper/*.xml")); return sessionFactory.getObject(); } } ``` 4. 创建一个User实体类,包含id、username和password字段。 ``` public class User { private Long id; private String username; private String password; // getter和setter方法省略 } ``` 5. 创建一个Role实体类,包含id和roleName字段。 ``` public class Role { private Long id; private String roleName; // getter和setter方法省略 } ``` 6. 创建一个Permission实体类,包含id和permissionName字段。 ``` public class Permission { private Long id; private String permissionName; // getter和setter方法省略 } ``` 7. 创建一个UserMapper接口,定义对User表的操作。 ``` @Mapper public interface UserMapper { User findByUsername(String username); } ``` 8. 创建一个RoleMapper接口,定义对Role表的操作。 ``` @Mapper public interface RoleMapper { List<Role> findByUserId(Long userId); } ``` 9. 创建一个PermissionMapper接口,定义对Permission表的操作。 ``` @Mapper public interface PermissionMapper { List<Permission> findByRoleId(Long roleId); } ``` 10. 创建一个UserService类,添加以下内容: ``` @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; public User findByUsername(String username) { return userMapper.findByUsername(username); } public List<Role> findRolesByUserId(Long userId) { return roleMapper.findByUserId(userId); } public List<Permission> findPermissionsByRoleId(Long roleId) { return permissionMapper.findByRoleId(roleId); } } ``` 11. 创建一个JwtUtils类,添加以下内容: ``` public class JwtUtils { private static final String SECRET_KEY = "mysecretkey"; // 密钥 private static final long EXPIRATION_TIME = 60 * 60 * 24 * 7; // 过期时间,单位为秒 // 生成JWT public static String generateToken(String username) { Date now = new Date(); Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME * 1000); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 解析JWT public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } // 验证JWT public static boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } } ``` 12. 创建一个JwtRealm类,继承AuthorizingRealm类,添加以下内容: ``` public class JwtRealm extends AuthorizingRealm { @Autowired private UserService userService; // 支持JwtToken @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); List<Role> roles = userService.findRolesByUserId(1L); // 假设用户ID为1 List<String> roleNames = roles.stream().map(Role::getRoleName).collect(Collectors.toList()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRoles(roleNames); for (Role role : roles) { List<Permission> permissions = userService.findPermissionsByRoleId(role.getId()); List<String> permissionNames = permissions.stream().map(Permission::getPermissionName).collect(Collectors.toList()); authorizationInfo.addStringPermissions(permissionNames); } return authorizationInfo; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException(); } return new SimpleAuthenticationInfo(username, user.getPassword(), getName()); } } ``` 13. 创建一个JwtToken类,实现AuthenticationToken接口,添加以下内容: ``` public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return JwtUtils.parseToken(token).getSubject(); } @Override public Object getCredentials() { return token; } } ``` 14. 创建一个JwtFilter类,继承BasicHttpAuthenticationFilter类,添加以下内容: ``` public class JwtFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { String token = getToken(request); if (token == null || !JwtUtils.validateToken(token)) { return false; } JwtToken jwtToken = new JwtToken(token); try { getSubject(request, response).login(jwtToken); return true; } catch (Exception e) { return false; } } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) { response.setContentType("application/json;charset=UTF-8"); try { PrintWriter out = response.getWriter(); out.write("{\"code\":401,\"message\":\"未登录\"}"); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } return false; } private String getToken(ServletRequest request) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer ")) { return token.substring(7); } return null; } } ``` 15. 创建一个UserController类,添加以下内容: ``` @RestController public class UserController { @Autowired private UserService userService; @PostMapping("/login") public String login(String username, String password) { User user = userService.findByUsername(username); if (user == null || !user.getPassword().equals(password)) { return "{\"code\":401,\"message\":\"用户名或密码错误\"}"; } String token = JwtUtils.generateToken(username); return "{\"code\":200,\"message\":\"登录成功\",\"token\":\"" + token + "\"}"; } @GetMapping("/user") @RequiresPermissions("user:view") public String viewUser() { return "View User"; } } ``` 以上是一个简单的demo项目,实现了基于Spring Boot集成Shiro和JWT进行权限控制,使用MyBatis操作数据库。需要注意的是,这里的代码仅供参考,具体实现还需要根据实际情况进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值