一文搞定Shiro+EhCache,实现对认证、鉴权信息缓存

2 篇文章 0 订阅

shiro+JWT+EhCache,实现认证、鉴权缓存的移除与更新。

前言

基本上一个正经的管理系统,都会带有有较细粒度的权限过滤,通常都是使用RBAC模式进行权限的设计,也就是:权限-角色-用户。
一个用户可以拥有多个角色,一个角色拥有多个权限。
用户不直接与权限关联,而是与角色关联
同时,实际开发中,为了提高鉴权的效率,一般也会对用户的认证信息与权限信息进行缓存操作,为了满足权限共享,多使用Redis,不过这里讲的是使用EhCache做缓存服务,换成redis实际上也就是更换实现。
本文基于springboot编写,所涉及的entity、service,以及权限表需自行定义

流程解释

用户登录时,需要自定义登录逻辑,如是否需要验证码等,登录成功后,使用JWT工具生成token并返回,,前端需要保存返回的token,后续的请求需要携带此token。
除了登录操作和被放行的可直接访问的资源以外,一个请求在执行真正业务之前,一般都会经过一层过滤器的过滤,过滤器中会对当前的请求进行一个登录认证操作。
首先是判断此次请求有无携带token,如未携带,表明该请求未登录,直接做出相应返回,如跳转登录页,或是返回指定错误信息。
以上只是本次请求是否可行的判断,而真正的权限鉴定,一般都在真正访问接口前进行
如权限鉴定和角色鉴定
流程图:在这里插入图片描述

代码

组件介绍
BasicHttpAuthenticationFilter-过滤器

先要明确一点,当shiro执行了此Filter时,就已经证明了所请求的资源是需要验证的

方法解释

通常都是自定义一个过滤器继承该类,并重写几个特定方法,如
preHandle 预处理方法,可用于解决跨域问题
isAccessAllowed是否允许本次请求访问资源,允许则返回true
isLoginAttempt判断是否需要执行登录操作,一般可在isAccessAllowed中使用,判断本次是否执行登录操作,而判断的依据可以是检查请求是否携带token
executeLogin执行登录验证,若本次请求携带token,则在isAccessAllowed中执行登录操作
onAccessDenied拒绝请求,若本次请求未带token,或者登录验证失败,则执行此方法,可在此方法中返回指定数据给本次请求,如web端跳转登录页,或前后分离时返回登录code
preHandle->isAccessAllowed->isLoginAttempt->executeLogin->onAccessDenied

访问流程图

图中执行链以调用开头的,都是自己执行调用的,其余的为shiro执行
在这里插入图片描述

代码
    /**
     * 判断用户是否需要登入。
     * 检测header或cookie里是否包含token字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader(JWT_TOKEN_HEADER_NAME);//从header获取token
        Cookie[] cookies = null;//准备从cookie获取token
        if (StringUtils.isBlank(authorization) && ArrayUtils.isNotEmpty((cookies=httpServletRequest.getCookies()))) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(JWT_TOKEN_COOKIE_NAME)) {
                    authorization = StringUtils.isNotBlank(cookie.getValue()) ? cookie.getValue() : null;
                 break;
                }
            }
        }
        request.setAttribute("token",authorization);//若token位置唯一,则不用再存一次,直接返回ture,在executeLogin再获取即可
        return !StringUtils.isBlank(authorization);
	}
	//执行登录操作
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        Object tokenAttr = request.getAttribute("token");//isLoginAttempt中已对token位置做处理
        JWTToken token = new JWTToken(tokenAttr.toString());//新建一个token对象
        if (token.getPrincipal()==null) return false;//若token创建失败,直接退出
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request,response).login(token);//让shiro执行登录操作,最终会走Realm的验证方法,失败则抛异常
        return true;
    }
    //shiro调用此方法判断本次请求是否被允许
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {//判断是否需要执行登录,不执行,直接返回false
            try {
                return executeLogin(request, response);//执行登录操作
            } catch (Exception e) {
                request.setAttribute("msg", e.getMessage());
            }
        }
        return false;
    }
	//当本次请求不被允许时,shiro会调用此方法
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        String uri = httpServletRequest.getRequestURI();
        if (uri.startsWith(WEB_PAGE_PATH_PREFIX)) {//判断是否是web网页请求,是则跳转login页面
        //可对本次请求的uri进行处理,保证登录完成后再次回到本页面
            RequestDispatcher requestDispatcher = ((HttpServletRequest) request).getRequestDispatcher(login_html);
            requestDispatcher.forward(request, response);
        }else {//前后分离,则可回写状态码
            response.setCharacterEncoding("utf-8");
            OutputStream os = response.getOutputStream();
            os.write(JSONObject.toJSONBytes(error_json)));
            os.flush();
            os.close();
        }
        return false;
    }
     //预处理,对跨域提供支持
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
一般实体
AuthenticationToken

shiro验证过程中,用于携带token和用户唯一凭证的实例

public class JWTToken implements AuthenticationToken {
    // 密钥
    private String token;
    private String userId;
    public JWTToken(String token) {
        this.token = token;
        this.userId =JWTUtil.getUserId(this.token);//对token进行解析,获取用户唯一标识
    }
    //获取标识
    public Object getPrincipal() {
        return userId;//后续可用于判断token是否解析正常
    }
    //获取密钥,即token
    public Object getCredentials() {
        return token;
    }
}
PrincipalCollection

凭证集合,shiro已提供子类,可满足正常需求:SimplePrincipalCollection
同时,这里的Principal与token中的Principal意义不同,token中的Principal仅是为了方便后续判断的标识,而真正的PrincipalCollection中存储的Principal,是后续业务逻辑中,可能会用到的必要用户信息,如用户实体,其余用户信息等

AuthorizationInfo

用户验证信息,shiro已提供子类,可满足正常需求:SimpleAuthenticationInfo
其中存储了用户的凭证集合

AuthorizationInfo

用于存储用户权限集合信息,包含角色,权限等信息,已有可用子类:SimpleAuthorizationInfo

AuthorizingRealm真正登录及鉴权的工具
需重写方法

supports:使自定义的token被shiro支持
doGetAuthorizationInfo:获取权限
doGetAuthenticationInfo:验证信息
getAuthorizationCacheKey获取权限存于缓存中的key值,自定义规则,简化后续操作
getAuthenticationCacheKey:获取认真信息存于缓存中的key值,自定义规则,简化后续操作

@Component
public class MyRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private ManagerService managerService;

    @Autowired
    @Lazy
    private RoleService roleService;
    @Autowired
    @Lazy
    private CacheUtil cacheUtil;

    @Autowired
    @Lazy
    private MenuService menuService;


    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException, CommonBaseException {
        // 解密获得username,用于和数据库进行对比
        String userId = authToken.getPrincipal().toString();
        if (userId == null)//判断token数据是否有效
            throw new AuthenticationException(Constant.ADMIN_LOGIN_TIMEOUT);
        Manager manager = managerService.getById(userId);//从数据库中检出用户
        if (manager == null)//判断用户是否存在
            throw new AuthenticationException(Constant.ADMIN_MANAGER_NOT_EXIST);
        if (manager.getStatus() == 0)//判断用户状态
            throw new AuthenticationException(CommonBaseErrorCode.ADMIN_MANAGER_IS_LOCK.getErrMsg());
        String token = authToken.getCredentials().toString();//验证token的真实有效性
        if (!JWTUtil.verify(token, userId, manager.getPassword()))
            throw new AuthenticationException(Constant.ADMIN_USERNAME_PASSWORD_ERROR);
        //返回用户验证信息,其中,库中数据作为主要principal存入,并且存入token,并标记本次验证的realmName,因为shiro支持多realm验证。
        return new SimpleAuthenticationInfo(manager, token, Constant.MY_REALM);
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //解密用户名
        Object primaryPrincipal = principals.getPrimaryPrincipal();//获取验证用户时存入的主凭证
        if (primaryPrincipal == null)//判空
            throw new AuthenticationException(Constant.ADMIN_LOGIN_TIMEOUT);
        Manager manager = (Manager) primaryPrincipal;//强转
        //查询用户所拥有角色
        List<Role> roles = roleService.getUserRole(manager.getId().toString());//角色service获取用户Roles
        manager.setRoles(roles);//存入manager实体中(看需求存入)
        //设置用户角色
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(
                new ArrayList<String>() {{
                    roles.forEach(r -> add(r.getRoleName()));
                }});//遍历获取到的角色,将角色的唯一标识建立集合
        //查询用户角色拥有的菜单(权限)
        List<Menu> menus = menuService.findMenuByRIds(
                new ArrayList<Integer>() {{
                    roles.forEach(role -> add(role.getId()));
                }}, 1);
        //设置用户菜单权限
        ArrayList<String> permissions = new ArrayList<String>() {{
            menus.forEach(menu -> add(menu.getPermission()));
        }};//同Role操作
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }




    //获取用户授权缓存key
    protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
//        已知主principal为用户实体,这里直接强转,并用ID作为缓存key
        return ((Manager) principals.getPrimaryPrincipal()).getId();
    }
    //获取用户缓存key
    protected Object getAuthenticationCacheKey(AuthenticationToken token) {
        //已知token中的Principal就是userId,唯一标识,做key,后续方便操作缓存
        return token != null ? token.getPrincipal() : null;
    }
}

配置信息

maven引入
		<!--便捷entity-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
            <scope>provided</scope>
        </dependency>
		<!-- apache组件包commons -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
		<!--JWT验证-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!--shiro依赖-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- ehchache -->
         <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
        <!-- springBoot整合ehchache -->        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- shiro整合ehchache -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>
Config配置
EhCacheConfig
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfig {
    @Bean
    public EhCacheManager ehCacheManager(CacheManager cacheManager) {
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManager(cacheManager);
        return ehCacheManager;
    }
}

XML文件

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
    <!--缓存对象存放路径-->
    <diskStore path="java.io.tmpdir"/>
    <!--默认缓存配置-->
    <defaultCache maxElementsInMemory="10000"  eternal="false"
            timeToIdleSeconds="0" timeToLiveSeconds="0"
            overflowToDisk="false" diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120" />
    <!-- 授权缓存 -->
    <cache name="authorizationCache" maxEntriesLocalHeap="2000"
           eternal="false" timeToIdleSeconds="0"
           timeToLiveSeconds="0"  overflowToDisk="false"
           statistics="true"> </cache>
    <!-- 认证缓存 -->
    <cache name="authenticationCache" maxEntriesLocalHeap="2000"
           eternal="false"  timeToIdleSeconds="0"
           timeToLiveSeconds="0" overflowToDisk="false"
           statistics="true"> </cache>
</ehcache>
ShiroConfig

配置了缓存后,shiro会自动以realm中重写的缓存key,从所配置的缓存中获取数据,如果为空,则调用realm的鉴权或认证方法,完成操作后,会自动存入缓存中

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(MyRealm myRealm,EhCacheManager ehCacheManager) {
        myRealm.setCachingEnabled(true);
        //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
        myRealm.setAuthenticationCachingEnabled(true);
//        缓存AuthenticationInfo信息的缓存名称 在ehcache.xml中有对应缓存的配置
        myRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存,即缓存AuthorizationInfo信息,默认false
        myRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称  在ehcache.xml中有对应缓存的配置
        myRealm.setAuthorizationCacheName("authorizationCache");
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(myRealm);
        manager.setCacheManager(ehCacheManager);
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
		//设置登录页面
        factoryBean.setLoginUrl("/login.html");
        //设置登录成功后跳转的页面
        factoryBean.setSuccessUrl("/");
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
        //拦截器
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        //配置映射关系 anon即不验证,jwt即为刚刚添加的过滤器,
        // /**为除了被放行的资源,其余都要经过jwt过滤器
        filterChainDefinitionMap.put("/static/**","anon");//放行静态资源
        filterChainDefinitionMap.put("/login", "anon");//放行登录操作
        // 所有请求通过JWT Filter做验证
        filterChainDefinitionMap.put("/**", "jwt");
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }
    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
工具类Util
JWTUtil
public class JWTUtil {
    /**
     * 校验token是否正确
     */
    public static boolean verify(String token, String userId, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("userId", userId).build();
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }
    /**
     * 获得token中的信息,无需secret解密也能获得
     */
    public static String getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    /**
     * 生成签名,指定天数后过期
     */
    public static String sign(String username, String secret) {
        Date date = new Date(System.currentTimeMillis()+ EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create().withClaim("userId", username)
                .withExpiresAt(date).sign(algorithm);
    }
    /**
     * 加密用户密码
     */
    public static String encryptPassword(String password ){
        return new SimpleHash("MD5",password,password.substring(password.length()-2),password.length()).toString();
    }
}

其他

shiro的login简述

当在filter中调用getSubject时,会从当前线程中获取一个ThreadContext对象,这个对象将绑定一个ThreadLocal,在执行login操作时,shiro会调用realm中的认证方法,并将成功认证后的结果绑定到ThreadLocal中。
在后续的代码中,若有地方需要从shiro中获取当前认证信息,也就是用户信息时,只需要从SecuretyUtil中get当前的Subject,便可以获取到,在filter的login操作时,已绑定到线程中的认证信息。

缓存的过程
认证缓存

当用户登录时,shiro的login会从缓存中,尝试利用token,利用realm中的认证缓存key方法,获取缓存key,再去已在config中配置的cache中获取认证缓存,若缓存为空,再真正调用realm中的认证方法,并将成功登录的结果存入缓存中,以供下次获取。

权限缓存

当一个用户发起请求,并且该请求已被放行,访问到了真正的接口,同时该接口有权限要求时,shiro会执行realm中鉴权方法,并将结果缓存,以待下次使用。
所以当一个接口有权限限制时,必须配置权限声明,不然任意用户成功通过filter登录后,都能成功访问该接口。

注意事项

若shiro使用到了缓存,而一个用户的登录凭证(token)又由用户存储时,当我们变更用户权限或甚至是锁定用户,我们就必须要及时的对用户缓存数据进行清除,若不然,当用户再次发起请求时,只会从缓存中获取失效的数据。
当对用户角色做变更时,需要删除该用户的缓存的缓存。
当对角色的权限变更候,需要删除所有拥有该角色的用户的缓存。
当对用户进行锁定操作时,需要删除用户认证及权限缓存。
当对权限进行变更时,需要找到所有拥有该权限的角色,删除所有拥有这些角色的用户的权限缓存

权限鉴定

shiro提供了几种注解用于鉴权,注解于控制单元
用户必须登录:@RequiresAuthentication
用户必须有以下权限: @RequiresPermissions(“权限名,realm鉴权时加入的集合中的菜单”)
用户必须为以下角色:@RequiresRoles(“角色名”)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,由于篇幅和涉及版权等问题,无法提供完整的代码实现。以下是基于springboot+shiro+layui实现qq聊天室功能的代码结构和主要实现思路供参考: 1. 后端代码结构 - controller:控制层,实现用户登录、注册等接口。 - service:服务层,实现用户管理、聊天消息管理等业务逻辑。 - dao:数据访问层,实现对数据库的增删改查操作。 - entity:实体类,包括用户信息、聊天消息等。 - config:配置类,实现Shiro安全框架的配置。 - websocket:WebSocket实现,接收消息并广播给在线用户。 2. 前端代码结构 - index.html:聊天室主页面。 - login.html:用户登录页面。 - register.html:用户注册页面。 - layui:Layui框架相关文件。 - js:前端JS代码,实现聊天室界面和逻辑。 3. 主要实现思路 - 用户登录:前端发送用户名和密码到后台,后台进行密码验证并返回登录结果。 - 用户注册:前端发送用户名和密码到后台,后台将用户信息保存到数据库中。 - 聊天室界面:前端使用Layui实现聊天室界面,包括聊天消息显示和发送消息等功能。 - 实时通讯:使用WebSocket实现前后端实时通讯功能,后端接收消息并将消息广播给所有在线用户,前端接收消息并显示在聊天室界面中。 - 私聊功能:在聊天室界面中添加私聊功能,用户可以选择一个在线用户进行私聊,后端接收私聊消息并发送给对应的用户。 - 数据库存储:使用MySQL作为持久化存储,将用户信息、聊天记录等数据保存到数据库中。 以上是springboot+shiro+layui实现qq聊天室功能的主要实现思路和代码结构,具体实现过程需要根据实际需求进行调整和改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值