ruoyi-cloud开源框架集成cas

ruoyi-cloud框架集成cas及权限管理

背景:

公司老系统的认证全部在cas当中,而新系统使用了ruoyi-cloud开源框架。为了能够实现用户一次登陆,就能在新老系统之间免登录相互访问的效果,需在对ruoyi-cloud框架当中集成cas。

整体逻辑

CAS为对通过认证的用户分配ticket,通过发放ticket对用户进行管理,而ruoyi-cloud的认证及权限的管理则是依靠ticket完成的。所有解决问的核心逻辑:

1、cas对访问ruoyi-cloud但不持有ticket的用户进行拦截,用户需在cas进行认证。

2、用户在cas完成认证后,在ruoyi-cloud后端自动为用户生产token,全过程对用户无感知。

3、完成1、2两步之后,用户即持有老系统的ticket,又持有ruoyi-cloud的token。

具体cas集成认证步骤

如下图所示

核心代码

前端:

  • permission.js,路由守卫
    • 若访问资源的用户没有token,则向后端store.dispatch('casLogin')
    • 待store.dispatch('Login')返回之后,根据拉取路由,在前端进行展示,注意:对于store.dispatch('casLogin')后then里的方法有以下两点:1、对于没ticket的用户cas拦截注册之后,后端会重定向到前端,所有不进then下面的方法。2、重定向第二次进到这个方法,用户有ticket了,才会进到then下面的代码
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'

NProgress.configure({ showSpinner: false })

const whiteList = ['/login', '/auth-redirect', '/bind', '/register']

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(() => {
          store.dispatch('GenerateRoutes').then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            router.addRoutes(accessRoutes) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
            store.dispatch('LogOut').then(() => {
              Message.error(err)
              next({ path: '/' })
            })
          })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      store.dispatch('casLogin').then(res => {
        // 拉取user_info,注意此处只有在cas完成认证后才会进来:1、对于没ticket的用户cas拦截注册之后,后端会重定向到前端,所有不进下面的方法。2、重定向第二次进到这个方法,用户有ticket了,才会进到这个里面
        const roles = res.roles
        store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
          // 根据roles权限生成可访问的路由表
          router.addRoutes(accessRoutes) // 动态添加可访问路由表
          next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          NProgress.done()
        })
      }).catch(err => {})
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})
  • user.js 新增casLogin
    • 对后端的返回结果进行处理,在cookie里写入token信息和
    • 存储用户的角色、权限信息
  • user.js 新增casLogout
    • call后端casLogin接口,删除redis里的token
    • 清空token、角色及权限信息
    • 跳到后端所配置的cas登出的后端地址
    casLogin({ commit, state }) {
      return new Promise((resolve, reject) => {
        casLogin().then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          const user = res.user
          const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : user.avatar;
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_NAME', user.userName)
          commit('SET_AVATAR', avatar)
          resolve(res)

        }).catch(error => {
          reject(error)
        })
      })
    }



    casLogout({ commit, state }) {
      return new Promise((resolve, reject) => {
        logout(state.token).then(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSIONS', [])
          removeToken()
          window.location.replace("http://所配置的cas登出的后端地址")
        }).catch(error => {
          reject(error)
        })
      })
    },
  • login.js,新增casLogin函数
export function casLogin() {
  return request({
    url: '/auth/casLogin',
    method: 'get'
  })
}
  • request.js 修改响应拦截器
    • 当code为401,直接重定向到cas登陆网页,跳过用户点击确认的步骤
  • request.js
    • 修改判断逻辑if(value!==null && typeof(value) !== "undefined") 改为 if(value && typeof(value) !== "undefined"))
// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;

    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    if (code === 401) {
        // 单点登录时使用
        window.location.replace(res.data.loginUrl)
    } else if (code === 500) {
      Message({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      Notification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
      return res.data
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    Message({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)


// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?';
    for (const propName of Object.keys(config.params)) {
      const value = config.params[propName];
      var part = encodeURIComponent(propName) + "=";
      if (value && typeof(value) !== "undefined") {
        if (typeof value === 'object') {
          for (const key of Object.keys(value)) {
            let params = propName + '[' + key + ']';
            var subPart = encodeURIComponent(params) + "=";
            url += subPart + encodeURIComponent(value[key]) + "&";
          }
        } else {
          url += part + encodeURIComponent(value) + "&";
        }
      }
    }
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

后端

  • auth模块bootstrap.yml
app:
  login:
    url: /login
  logout:
    url: /logout
  server:
    host:
      url: http://auth模块地址
cas:
  server:
    host:
      login_url: ${cas.server.host.url}/login
      logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
      url: http://cas地址/cas
  • 新增CASAuthenticationEntryPointImpl.java
    • 重写commence函数
       
package com.ruoyi.auth.cas;

import com.alibaba.fastjson.JSON;
import com.ruoyi.common.core.constant.HttpStatus;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

@Component
public class CASAuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Autowired
    private CasProperties casProperties;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException
    {
        StringBuffer requestURL = request.getRequestURL();
        System.out.println("requestURL=>"+requestURL);
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        AjaxResult error = AjaxResult.error(code, msg);
        error.put("loginUrl", casProperties.getCasServerLoginUrl()+"?service="+casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        ServletUtils.renderString(response, JSON.toJSONString(error));
    }
}
 
  • 新增cas配置类CasSecurityConfig.java
    • 主要配置cas拦截策略
package com.ruoyi.auth.cas;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Logger log = LoggerFactory.getLogger(CasSecurityConfig.class);

    @Autowired
    private CasProperties casProperties;

    /**
     * 认证失败处理类
     */
    @Autowired
    private CASAuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 定义认证用户信息获取来源,密码校验规则等
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth.authenticationProvider(casAuthenticationProvider());
    }

    /**
     * 定义安全策略
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 认证失败处理类
                .authorizeRequests()//配置安全策略
                .antMatchers("/login", "/captchaImage").permitAll()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/"
                ).permitAll()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()

                // activiti modeler 放行
                .antMatchers("/modeler/**").anonymous()
                .antMatchers("/activiti/definition/upload").anonymous()
                .antMatchers("/activiti/definition/readResource").anonymous()
                .antMatchers("/activiti/process/read-resource").anonymous()

                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated().and()

                //.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint()).and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .addFilter(casAuthenticationFilter())
                .addFilterBefore(casLogoutFilter(), LogoutFilter.class)
                .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
    }

    /**
     * 认证的入口
     */
    @Bean
    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
        return casAuthenticationEntryPoint;
    }

    /**
     * 指定service相关信息
     */
    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    /**
     * CAS认证过滤器
     * 判断是否已经登录,如果没有登录则根据配置的信息来决定将跳转到什么地方
     */
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
        return casAuthenticationFilter;
    }

    /**
     * cas 认证 Provider
     */
    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setKey("casAuthenticationProviderKey");
        return casAuthenticationProvider;
    }

    /**
     * 用户自定义的AuthenticationUserDetailsService
     */
    @Bean
    public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService() {
        return new CustomUserDetailsService();
    }

    /**
     * 配置ticket校验器
     *
     * @return
     */
    @Bean
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {

        return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
    }

    /**
     * 单点登出过滤器
     */
    @Bean
    public SingleSignOutFilter singleSignOutFilter() {

        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
        singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
        singleSignOutFilter.setIgnoreInitConfiguration(true);
        return singleSignOutFilter;
    }

    /**
     * 请求单点退出过滤器
     */
    @Bean
    public LogoutFilter casLogoutFilter() {

        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
        return logoutFilter;
    }

}

 

  • 新增CustomUserDetailsService.java
    • 重写loadUserDetails函数,load用户信息,主要是用户名称。
package com.ruoyi.auth.cas;

import com.ruoyi.auth.service.SysLoginService;
import com.ruoyi.common.security.service.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.HashSet;

/**
 * 用于加载用户信息 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
 *
 * @author xu
 */
public class CustomUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {

    private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);

    @Autowired
    private SysLoginService sysLoginService;

    @Autowired
    private TokenService tokenService;

    /**
     * 加载登录用户的信息
     *
     * @param token
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
        System.out.println("当前的用户名是:" + token.getName());
        String username = token.getName();
        return new User(username, "", new HashSet<>());
    }
}
  • 修改auth模块的启动函数
    • cas完成认证之后,会跳转到"/"接口,重定向到前端,这样cas认证完成后转到前端逻辑,前端会再call一遍后端接口,执行后续的为用户生产token、加载权限和角色信息
package com.ruoyi.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import com.ruoyi.common.security.annotation.EnableRyFeignClients;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证授权中心
 *
 * @author ruoyi
 */
@EnableRyFeignClients
@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        SecurityAutoConfiguration.class,
})
@RestController
public class RuoYiAuthApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(RuoYiAuthApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  认证授权中心启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.       ____     __        \n" +
                " |  _ _   \\      \\   \\   /  /    \n" +
                " | ( ' )  |       \\  _. /  '       \n" +
                " |(_ o _) /        _( )_ .'         \n" +
                " | (_,_).' __  ___(_ o _)'          \n" +
                " |  |\\ \\  |  ||   |(_,_)'         \n" +
                " |  | \\ `'   /|   `-'  /           \n" +
                " |  |  \\    /  \\      /           \n" +


                " ''-'   `'-'    `-..-'              ");
    }

    @CrossOrigin
    @RequestMapping("/")
    public void index(HttpServletResponse response) throws IOException {
        response.sendRedirect("http://前端地址");
    }
}
  • TokenController.java 新增 casLogin函数
    • cas在完成认证后,前端第二次call后端接口,会进入此函数。
    • 为用户生产token,并写redis。
    • 加载用户角色和权限信息。
    • 通过ajax请求将toke、角色和权限信息吐还给前端
    @GetMapping("casLogin")
    public AjaxResult casLogin()
    {
        UserDetails loginUser = (UserDetails) SecurityUtils.getLoginUser();
        AjaxResult ajax = AjaxResult.success();
        LoginUser userInfo = sysLoginService.login(loginUser.getUsername(), null);
        Map<String, Object> tokenMap = tokenService.createToken(userInfo);
        ajax.put("user", userInfo.getSysUser());
        ajax.put("roles", userInfo.getRoles());
        ajax.put("permissions", userInfo.getPermissions());
        ajax.put("token",tokenMap.get("access_token"));
        return ajax;
    }
  • SysLoginService.java 修改login函数
    • cas已经对账号密码进行了校验,此处不用对密码进行校验
package com.ruoyi.auth.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.system.api.RemoteLogService;
import com.ruoyi.system.api.RemoteUserService;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;

/**
 * 登录校验方法
 * 
 * @author ruoyi
 */
@Component
public class SysLoginService
{
    @Autowired
    private RemoteLogService remoteLogService;

    @Autowired
    private RemoteUserService remoteUserService;

    /**
     * 登录
     */
    public LoginUser login(String username)
    {
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
            remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
            throw new BaseException("用户名不在指定范围");
        }
        // 查询用户信息
        R<LoginUser> userResult = remoteUserService.getUserInfo(username);

        if (R.FAIL == userResult.getCode())
        {
            throw new BaseException(userResult.getMsg());
        }

        if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
        {
            remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
            throw new BaseException("登录用户:" + username + " 不存在");
        }
        LoginUser userInfo = userResult.getData();
        SysUser user = userResult.getData().getSysUser();
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");

            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }
        remoteLogService.saveLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
        return userInfo;
    }

    public void logout(String loginName)
    {
        remoteLogService.saveLogininfor(loginName, Constants.LOGOUT, "退出成功");
    }
}
  • 修改ruoyi-gateway-dev.yml
    • 增加casLogin到witeList
ignore:
  whites:
    - /auth/logout
    - /auth/login
    - /*/v2/api-docs
    - /csrf
    - /auth/casLogin

参考文章:

cas集成若依前后端分离版本:https://blog.csdn.net/qq_42017523/article/details/117018648

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
RuoYi-Cloud是一套基于Spring Cloud技术的分布式微服务开发框架,专为企业级应用开发而设计。该框架整合了各种常用的微服务组件,如注册中心、配置中心、网关、消息队列等,使得开发人员可以快速搭建起一个完整的分布式系统。 RuoYi-Cloud的视频教程可以在CSDN(中国最大的IT技术社区和交流平台)上找到。这些视频教程的目的是帮助开发人员更好地理解和使用RuoYi-Cloud框架。视频内容包括RuoYi-Cloud的特点、架构设计、常用功能实现以及实战案例等。 通过观看这些视频教程,开发人员可以系统地学习RuoYi-Cloud框架的各个方面,包括框架的理论基础和实际应用。视频教程以实际操作为主,通过演示和讲解的方式帮助开发人员更好地掌握RuoYi-Cloud的使用方法和技巧。 对于想要使用RuoYi-Cloud框架进行分布式微服务开发的开发人员来说,这些视频教程是非常有价值的学习资源。通过学习这些视频教程,开发人员可以节省大量的学习和探索时间,快速上手使用RuoYi-Cloud框架进行项目开发。同时,这些视频教程也可以帮助开发人员深入了解和理解分布式微服务架构的应用场景和优势。 总之,RuoYi-Cloud的视频教程提供了学习和使用RuoYi-Cloud框架的有力支持和指导,对于开发人员来说是非常有帮助的资源,通过学习这些教程可以加快开发人员对RuoYi-Cloud框架的掌握程度,从而提高开发效率和质量。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值