楚新元 | All in R

Welcome to R Square

分享一个 shinyApp 的用户和角色权限管理的方案

楚新元 / 2023-03-09


最近做了一个自动生成文档的 shinyApp,顺便用 {shinyauthr} 包做了一个用户登录界面,但是要实现用户修改密码的功能困扰了我好久,主要问题是 session 一旦起来后只修改配置文件里的用户密码是不行了,因为内存中的密码没有变,后来我终于想到了解决方案,把读取配置文件放在 server function 里面,每次修改配置文件密码后立马通过 session$reload() 重启会话,这样就会重新读取配置文件的用户信息,从而刷新内存中的,问题算是解决了。为了方便其他人,当然这个其他人也有可能就是未来某一天的我。长话短说,放码出来先:

先生成配置文件,这个配置文件包括用户和密码信息。用户和密码信息放在 config.sqlite 数据库的 ser_group 表里(其实一开始是放在 .csv 文件的,因为有多个配置文件,不方便导入导出),可以用如下代码生成:

library(RSQLite)
user_group = data.frame(
  user = c("admin", "test"),
  password = c("admin", "666666"),
  permissions = c("admin", "standard"),
  name = c("admin", "test")
)
config = dbConnect(SQLite(), "config.sqlite")
dbWriteTable(
  conn = config,
  name = "user_group",
  value = user_group,
  overwrite = TRUE
)
dbDisconnect(config)

下面是一个带用户和角色权限管理的 shinyApp 的示例:

##----------------------------------------------------------------------------##

# 配置环境变量
# Sys.setlocale(category = "LC_ALL", locale = "zh_CN.UTF-8")
# Sys.setenv(RSTUDIO_PANDOC = "/usr/bin")
# options(bitmapType = "cairo")

##----------------------------------------------------------------------------##

# 加载相关R包
library(shiny)
library(shinydashboard)
library(shinyauthr)
library(shinyjs)
library(RSQLite)

##----------------------------------------------------------------------------##

# 定义前端交互界面
ui = dashboardPage(
  
  skin = "blue",
  dashboardHeader(
    title = "用户修改密码测试", 
    titleWidth = 260,
    tags$li(
      class = "dropdown", 
      style = "padding: 8px;", 
      logoutUI(
        id = "logout", 
        label = "",
        icon = icon("sign-out-alt"),
        class = "btn-primary",
        style = "color: white;"
      )
    )
  ),
  dashboardSidebar(
    width = 260,
    collapsed = TRUE,
    sidebarMenuOutput("sidebar")
  ),
  dashboardBody(
    useShinyjs(),
    loginUI(
      id = "login",
      title = "用户登录",
      user_title = "用户名",
      pass_title = "用户密码",
      login_title = "登录",
      error_message = "无效的用户名或密码!",
      additional_ui = NULL,
      cookie_expiry = 7
    ),
    tabItems(
      tabItem(
        tabName = "resetpassword",
        uiOutput("resetpassword_ui")
      )
    )
  )

)

##----------------------------------------------------------------------------##

# 定义后台服务逻辑
server = function(input, output, session) {
  
  # 获取配置文件信息
  config = dbConnect(SQLite(), "config.sqlite")
  user_group = dbGetQuery(config, "select * from user_group")
  dbDisconnect(config)
  
  # 定义凭证信息
  credentials <- loginServer(
    id = "login",
    data = user_group,
    user_col = user,
    pwd_col = password,
    log_out = reactive(logout_init())
  )
  
  logout_init <- logoutServer(
    id = "logout",
    active = reactive(credentials()$user_auth)
  )
  
  # 捕获登录用户信息
  user_info <- reactive({
    credentials()$info
  })
  
  # 登录后打开、退出时关闭侧边栏
  observe({
    if(credentials()$user_auth) {
      removeClass(
        selector = "body", 
        class = "sidebar-collapse")
    } else {
      addClass(
        selector = "body", 
        class = "sidebar-collapse")
    }
  })
  
  # 验证通过后才显示侧边菜单栏
  output$sidebar <- renderMenu({
    req(credentials()$user_auth)
    sidebarMenu(
      id = "tabs",
      menuItem(
        "修改密码",
        tabName = "resetpassword",
        icon = icon("key")
      )
    )
  })

  # 验证通过后才显示密码修改界面  
  output$resetpassword_ui <- renderUI({
    req(credentials()$user_auth)
    fluidPage(
      sidebarLayout(
        sidebarPanel(
          width = 3,
          passwordInput(
            inputId = "password1", 
            label = "请输入原密码:",
            value = ""
          ), 
          passwordInput(
            inputId = "password2", 
            label = "请输入新密码:",
            value = ""
          ), 
          passwordInput(
            inputId = "password3", 
            label = "请确认新密码:",
            value = ""
          ),
          br(),
          div(
            style = "display: inline-block;
                     width: 100%;
                     text-align: center;",
            actionButton(
              inputId = "button",
              label = "提交",
              # icon = icon("check"),
              class = "btn-primary btn-md"
            )
          ),
          tableOutput("resetinfo")
        ),
        mainPanel()
      )
    )
  })
  
  ##--------------------------------------------------------------------------##
  
  # 修改密码
  observeEvent(input$button, {
    pass1 = input$password1
    pass2 = input$password2
    pass3 = input$password3
    ## 验证密码后修改新密码
    if (pass1 == user_info()$password & pass2 != "" & pass2 == pass3) {
      user_group$password[user_group$user == user_info()$user] = pass2
      config = dbConnect(SQLite(), "config.sqlite")
      dbWriteTable(
        conn = config,
        name = "user_group",
        value = user_group,
        overwrite = TRUE
      )
      dbDisconnect(config)
      ### 密码修改成功后的反馈信息
      output$resetinfo <- renderTable(
        {
          print("密码已修改成功,请用新密码重新登录!")
        }, 
        colnames = FALSE
      )
      ### 重启会话以便读取新配置文件
      session$reload()
    } else {
      ### 密码不一致情况的反馈信息
      output$resetinfo <- renderTable(
        {
          if (pass1 != "" & pass1 != user_info()$password) {
            print("输入的原密码和登录密码不一致,请重新输入!")
          } else {
            if (pass2 != "" & pass2 != pass3) {
              print("输入的新密码和确认密码不一致,请重新输入!")
            } 
          }
        }, 
        colnames = FALSE
      )
    }
  })

}

##----------------------------------------------------------------------------##

# 运行shinyApp
shinyApp(ui = ui, server = server)

##----------------------------------------------------------------------------##