cnpm install @vue/[email protected]
版本号也是使用 @
分隔
如果缺少的是其他依赖也可以使用这样的方式解决
后端运行与配置
首先可以看一下若依官方文档给的项目结构介绍
主要模块是 admin,所有的接口都在这个模块,包括后面自己写的业务接口建议都放到这个模块
然后是system与common模块
system模块是将增删改查操作都放到里面了,做了分层,Service(提供给接口的服务层),Mapper(与数据库的操作,带xml),domain(JavaBean)
common模块比较杂,工具类都在这里面,包括:
Redis工具(直接与Redis操作)
SpringUtils(可以拿到SpringBoot的Context,getBean之类的)
ServletUtils(使用的ThreadLocal,可以拿到当前的请求与响应,接口参数之类的)
更多的需要自己发现了
framework模块是若依自己编写的包装(框架),这部分让我印象最深的就是权限认证了#-_-#,需求是做第三方登录,但要自定义登陆的话。卡了我一天多时间,主要原因是因为我没接触过Spring Security,关于有做第三方登陆需求的,这部分我会在这篇文章中将经验写出来。
还有几个模块,目前我的需求基本上不需要动这几部分,ruoyi-generator 这个代码生成,可以根据需要去修改配置(yml)
都是SpringBoot项目,首先进入 admin 模块,找到 application.yml,进入,根据自己的需要进行修改。
比较需要注意的地方是
ruoyi:profile(上传文件保存的地址)
spring:redis:(Redis的配置)
然后进入 application-druid.yml,这个是数据库的配置,其中要配置数据库的密码,数据库名称等看注释操作就行
在父模块文件夹中(RuoYi-Vue),有个sql文件夹,里面有两个sql,需要新建一个数据库进入,然后执行这两个sql
这样就可以运行后端了
运行后,启动成功的话可以在控制台看到启动成功的字样的
这样,将前面说前端也运行起来,就可以正常使用了。
还有很多地方可以进行配置,但在这里就不过多进行赘述了,熟悉SpringBoot的使用基本上没有什么太大压力。
首先可能需要配置的是后端的地址。在 vue.config.js
中
我们可能会发现,前端请求的后端地址路由前缀带上了 /dev-api
,这是因为做了多环境,使用了代理,后端并不是这样的,所以可以忽略这个。
其余的就是业务的编写了,组件部分都在 Component 文件夹,而页面布局在 layou 文件夹
我们自己写的页面在放 views 文件夹下。
可以先查阅官方文档,有很多使用上面都有写
首先需要建立一个sql表,表名称以在generator模块的yml配置前缀开头,如果没有更改的话就是 sys_
,可以有多个,使用逗号分隔
然后在系统工具 - 代码生成界面中导入表(可视化的,不多赘述了),有编辑预览和生成,编辑是设置生成代码的参数之类的,觉得没问题就生成
生成后的压缩包里有很多文件(大概好像是9个),包含了一个sql(菜单的增删改查sql语句以及权限等),后端controller、mapper、xml...前端 api.js、view...照着目录结构放到对应目录结构就可以了。
这部分官网文档上有,不过不是很显眼,我找了半天,在这里记录下
直接加上 @Anonymous
注解就可以了(前后端分离版),在类上加,类内的所有接口都可访问,在接口上加那就单独公开这个接口
使用 @Log
注解,可以参考若依里的代码,有很多地方都用到了
但需要注意的是,必须要有登录信息,也就是说公开接口不能使用这个注解。
@Log(title = "字典数据", businessType = BusinessType.EXPORT)
还有一个是登录日志,这个可以在登陆类中找到,直接照葫芦画瓢就行了。
若依是做了国际化的,但不多。用的 i18n,在 admin 模块的 resources 下可以看到 i18n 文件夹,有个message.properties 文件,其中包含了所有的文字....
使用的话就是
MessageUtils.message("message.properties中的key")
第三方登录注册
这篇文章记下这里就结尾吧,因为若依是使用了很多框架的一个快速开发框架。如果熟悉若依所使用的那些框架,那么上手起来没有什么太大的压力。
对于第三方登录,有一个特点,只会给你一个第三方的唯一用户标识,而在若依中,使用的Spring Security(这个我没学过,所以这里记录的可能并不是一个比较好的方法)。
我的方法如下
第三方登录,用户不存在就注册并登录,存在就直接登录
注册很简单,参考若依的注册接口 System 模块的 web.service.SysRegisterService
,但因为是第三方登录,没有账号密码,我的做法是生成一个随机的账号,与默认密码...因为照葫芦画瓢,登录是必须要账号和密码的
下面描述的是整个验证流程部分
首先查阅参考若依的登录接口,在 system 模块中 web.service.SysLoginService
它登陆部分代码是这样的
* 登录验证
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
public String login(String username, String password, String code, String uuid)
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
catch (Exception e)
if (e instanceof BadCredentialsException)
// 记录登录日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
finally
AuthenticationContextHolder.clearContext();
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
熟悉 Security 的看起来是非常简单的,核心部分就下面这三行。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
将账号和密码构建一个UsernamePasswordAuthenticationToken
,然后加入context,最后去调用验证,这是 Spring Security 的一个基础的做法
通过注释找到 UserDetailsServiceImpl 类,就在同一个包下,核心代码是这样的
(这部分我已经改过了,将这个类当作验证类了。)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
Object type = ServletUtils.getRequest().getAttribute("type");
if (type != null) {
switch ((int) type) {
case SecurityConfig.TYPE_OTHER:
passwordService.validateOther(user);
break;
} else {
passwordService.validate(user);
return createLoginUser(user);
看起来就好像是在这验证账号密码,虽然可以这样,但 Spring Security 定义的并不是这样的...
查阅 SecurityConfig 类,这个是 Spring Security 的配置类,在System模块的config下,我们需要在意的是最后一个函数,
* 身份认证接口
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
在这里如果没学过 Spring Security 就开始懵逼起来了,所以我先去学习了Spring Security,流程还是很简单,反正就是在这里设置的 UserDetailsService
因为是第三方登录,用户不需要输入账号密码,那么我拿到的密码就是加密后的密码,这样就和普通登录不一样了,就会导致认证不通过
经过 debug 后发现,首先走的是 AuthenticationManagerBuilder,这个不用管,只要知道这个里面有 ProviderManager 就行了,ProviderManager 是个管理器,具体的我也不记得了,它里面有AuthenticationProvider列表,而AuthenticationProvider这个东西就是需要解决掉的东西
private List<AuthenticationProvider> providers = Collections.emptyList();
Spring Security 默认使用的是 DaoAuthenticationProvider
,跟着debug可以看到
需要注意的就两个函数,一个additionalAuthenticationChecks,一个retrieveUser
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
return loadedUser;
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
catch (InternalAuthenticationServiceException ex) {
throw ex;
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
首先执行的是 retrieveUser 函数,这个函数会去调用若依的 UserDetailsServiceImpl 的 loadUserByUsername 函数
执行完后,会执行 additionalAuthenticationChecks 函数,这个函数进行账号密码验证,而若依是在 UserDetailsServiceImpl 的 loadUserByUsername 验证的,验证完后还会在验证一遍,因为账号密码的方式相同,所以不会有什么问题,但对我来说,传递的密码是加密的,所以不需要函数内进行加密,于是就要干掉这个类,在网上找了很久都找不到办法,就只能自己慢慢尝试了,这里直接写结果了
首先新建一个类,继承 DaoAuthenticationProvider 类,然后覆盖 additionalAuthenticationChecks 函数,在这个函数内做自己的操作。
然后还要覆盖一个函数 supports,这个是代表支持不支持当前传递的 authentication,例如UsernamePasswordAuthenticationToken
我直接返回 true 就行了,不支持的话那么就不会被覆盖
* 自定义
* 创建时间 2023年7月29日
* @author Shendi
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 有两种类型
Object type = ServletUtils.getRequest().getAttribute("type");
if (type != null) {
// 已经在 UserDetailsService 做了判断
switch ((int) type) {
case SecurityConfig.TYPE_OTHER:
break;
} else {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
String presentedPassword = authentication.getCredentials().toString();
if (!this.getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
/** 是否支持指定类型的 authentication */
public boolean supports(Class<?> authentication) {
// return (MyAuthenticationProvider.class.isAssignableFrom(authentication));
return true;
然后就是在 SecurityConfig 中配置了,直接上代码
@Bean
public MyAuthenticationProvider myAuP() {
MyAuthenticationProvider myAuP = new MyAuthenticationProvider();
myAuP.setUserDetailsService(userDetailsService);
myAuP.setPasswordEncoder(bCryptPasswordEncoder());
return myAuP;
* 身份认证接口
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.authenticationProvider(myAuP());
// auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());