• Spring Boot 集成Shiro的多realm配置


    转自:https://blog.csdn.net/cckevincyh/article/details/79629022

    我在做毕设的时候采用shiro进行登录认证和权限管理的实现。其中需求涉及使用三个角色分别是:学生、教师、管理员。现在要三者实现分开登录。即需要三个Realm——StudentRealm和TeacherRealm、AdminRealm,分别处理学生、教师和管理员的验证功能。

    但是正常情况下,当定义了多个Realm,无论是学生登录,教师登录,还是管理员登录,都会由这三个Realm共同处理。这是因为,当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法,源代码如下:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
    return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
    return doMultiRealmAuthentication(realms, authenticationToken);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    上述代码的意思就是如果有多个Realm就会使用所有配置的Realm。 只有一个的时候,就直接使用当前的Realm。

    为了实现需求,我会创建一个org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,让特定的Realm完成特定的功能。如何区分呢?我会同时创建一个org.apache.shiro.authc.UsernamePasswordToken的子类,在其中添加一个字段loginType,用来标识登录的类型,即是学生登录、教师登录,还是管理员登录。具体步骤如下(我的代码使用的是Groovy):

    enum LoginType {
    STUDENT("Student"), ADMIN("Admin"), TEACHER("Teacher")

    private String type

    private LoginType(String type) {
    this.type = type
    }

    @Override
    public String toString() {
    return this.type.toString()
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    接下来新建org.apache.shiro.authc.UsernamePasswordToken的子类UserToken


    import org.apache.shiro.authc.UsernamePasswordToken

    class UserToken extends UsernamePasswordToken {

    //登录类型,判断是学生登录,教师登录还是管理员登录
    private String loginType

    public UserToken(final String username, final String password,String loginType) {
    super(username,password)
    this.loginType = loginType
    }

    public String getLoginType() {
    return loginType
    }
    public void setLoginType(String loginType) {
    this.loginType = loginType
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    第三步:新建org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类UserModularRealmAuthenticator:

    import org.apache.shiro.authc.AuthenticationException
    import org.apache.shiro.authc.AuthenticationInfo
    import org.apache.shiro.authc.AuthenticationToken
    import org.apache.shiro.authc.pam.ModularRealmAuthenticator
    import org.apache.shiro.realm.Realm
    import org.slf4j.Logger
    import org.slf4j.LoggerFactory

    /**
    * 当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法
    *
    * 自定义Authenticator
    * 注意,当需要分别定义处理学生和教师和管理员验证的Realm时,对应Realm的全类名应该包含字符串“Student”“Teacher”,或者“Admin”。
    * 并且,他们不能相互包含,例如,处理学生验证的Realm的全类名中不应该包含字符串"Admin"。
    */
    class UserModularRealmAuthenticator extends ModularRealmAuthenticator {

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

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
    throws AuthenticationException {
    logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ")
    // 判断getRealms()是否返回为空
    assertRealmsConfigured()
    // 强制转换回自定义的CustomizedToken
    UserToken userToken = (UserToken) authenticationToken
    // 登录类型
    String loginType = userToken?.getLoginType()
    // 所有Realm
    Collection<Realm> realms = getRealms()
    // 登录类型对应的所有Realm
    Collection<Realm> typeRealms = new ArrayList<>()
    for (Realm realm : realms) {
    if (realm?.getName()?.contains(loginType))
    typeRealms?.add(realm)
    }

    // 判断是单Realm还是多Realm
    if (typeRealms?.size() == 1){
    logger.info("doSingleRealmAuthentication() execute ")
    return doSingleRealmAuthentication(typeRealms?.get(0), userToken)
    }
    else{
    logger.info("doMultiRealmAuthentication() execute ")
    return doMultiRealmAuthentication(typeRealms, userToken)
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    第四步:创建分别处理学生登录和教师登录、管理员登录的Realm:
    我这里直接贴出了我项目中的代码,你们可以根据具体的需求进行操作。
    AdminShiroRealm :

    package com.ciyou.edu.config.shiro.admin
    import com.ciyou.edu.config.shiro.common.UserToken
    import com.ciyou.edu.entity.Admin
    import com.ciyou.edu.service.AdminService
    import com.ciyou.edu.service.PermissionService
    import org.apache.shiro.authc.AuthenticationException
    import org.apache.shiro.authc.AuthenticationInfo
    import org.apache.shiro.authc.AuthenticationToken
    import org.apache.shiro.authc.SimpleAuthenticationInfo
    import org.apache.shiro.authc.UnknownAccountException
    import org.apache.shiro.authz.AuthorizationException
    import org.apache.shiro.authz.AuthorizationInfo
    import org.apache.shiro.authz.SimpleAuthorizationInfo
    import org.apache.shiro.realm.AuthorizingRealm
    import org.apache.shiro.subject.PrincipalCollection
    import org.apache.shiro.util.ByteSource
    import org.slf4j.Logger
    import org.slf4j.LoggerFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.context.annotation.Lazy


    class AdminShiroRealm extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class)
    @Autowired
    @Lazy
    private AdminService adminService


    @Autowired
    @Lazy
    private PermissionService permissionService

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    logger.info("开始Admin身份认证...")
    UserToken userToken = (UserToken)token
    String adminName = userToken?.getUsername() //获取用户名,默认和login.html中的adminName对应。
    Admin admin = adminService?.findByAdminName(adminName)

    if (admin == null) {
    //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
    throw new UnknownAccountException("用户不存在!")
    }

    //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    admin, //用户信息
    admin?.getPassword(), //密码
    getName() //realm name
    )
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(admin?.getAdminName())) //设置盐
    logger.info("返回Admin认证信息:" + authenticationInfo)
    return authenticationInfo
    }

    //当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

    logger.info("开始Admin权限授权(进行权限验证!!)")
    if (principals == null) {
    throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
    }
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
    if(principals?.getPrimaryPrincipal() instanceof Admin){
    Admin admin = (Admin) principals?.getPrimaryPrincipal()
    logger.info("当前Admin :" + admin )
    authorizationInfo?.addRole("Admin")
    //每次都从数据库重新查找,确保能及时更新权限
    admin?.setPermissionList(permissionService?.findPermissionByAdmin(admin?.getAdminId()))
    admin?.getPermissionList()?.each {current_Permission ->
    authorizationInfo?.addStringPermission(current_Permission?.getPermission())
    }
    logger.info("当前Admin授权角色:" +authorizationInfo?.getRoles() + ",权限:" + authorizationInfo?.getStringPermissions())
    return authorizationInfo
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    TeacherShiroRealm :

    package com.ciyou.edu.config.shiro.teacher

    import com.ciyou.edu.config.shiro.common.UserToken
    import com.ciyou.edu.entity.Teacher
    import com.ciyou.edu.service.TeacherService
    import org.apache.shiro.authc.*
    import org.apache.shiro.authz.AuthorizationException
    import org.apache.shiro.authz.AuthorizationInfo
    import org.apache.shiro.authz.SimpleAuthorizationInfo
    import org.apache.shiro.realm.AuthorizingRealm
    import org.apache.shiro.subject.PrincipalCollection
    import org.apache.shiro.util.ByteSource
    import org.slf4j.Logger
    import org.slf4j.LoggerFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.context.annotation.Lazy


    class TeacherShiroRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(TeacherShiroRealm.class)

    //在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
    //解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
    @Autowired
    @Lazy
    private TeacherService teacherService

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    logger.info("开始Teacher身份认证..")
    UserToken userToken = (UserToken)token
    String teacherId = userToken?.getUsername()
    Teacher teacher = teacherService?.findByTeacherId(teacherId)

    if (teacher == null) {
    //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
    throw new UnknownAccountException("用户不存在!")
    }

    //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    teacher, //用户信息
    teacher?.getPassword(), //密码
    getName() //realm name
    )
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(teacher?.getTeacherId())) //设置盐

    return authenticationInfo
    }

    //当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    logger.info("开始Teacher权限授权")
    if (principals == null) {
    throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
    }
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
    if(principals?.getPrimaryPrincipal() instanceof Teacher){
    authorizationInfo?.addRole("Teacher")
    return authorizationInfo
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    StudentShiroRealm :

    package com.ciyou.edu.config.shiro.student

    import com.ciyou.edu.config.shiro.common.UserToken
    import com.ciyou.edu.entity.Student
    import com.ciyou.edu.service.StudentService
    import org.apache.shiro.authc.*
    import org.apache.shiro.authz.AuthorizationException
    import org.apache.shiro.authz.AuthorizationInfo
    import org.apache.shiro.authz.SimpleAuthorizationInfo
    import org.apache.shiro.realm.AuthorizingRealm
    import org.apache.shiro.subject.PrincipalCollection
    import org.apache.shiro.util.ByteSource
    import org.slf4j.Logger
    import org.slf4j.LoggerFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.context.annotation.Lazy


    class StudentShiroRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(StudentShiroRealm.class)

    //在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
    //解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
    @Autowired
    @Lazy
    private StudentService studentService

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    logger.info("开始Student身份认证..")
    UserToken userToken = (UserToken)token
    String studentId = userToken?.getUsername()
    Student student = studentService?.findByStudentId(studentId)

    if (student == null) {
    //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
    throw new UnknownAccountException("用户不存在!")
    }

    //验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    student, //用户信息
    student?.getPassword(), //密码
    getName() //realm name
    )
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(student?.getStudentId())) //设置盐

    return authenticationInfo
    }

    //当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    logger.info("开始Student权限授权")
    if (principals == null) {
    throw new AuthorizationException("PrincipalCollection method argument cannot be null.")
    }
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo()
    if(principals?.getPrimaryPrincipal() instanceof Student){
    authorizationInfo?.addRole("Student")
    return authorizationInfo
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    接下来是对Shiro进行多realm的注解配置。
    这里直接贴出我项目中的代码。

    上面是我进行shiro进行配置的类,下面是主要的一些代码:


    //SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
    @Bean
    public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager()
    //设置realm.
    securityManager.setAuthenticator(modularRealmAuthenticator())
    List<Realm> realms = new ArrayList<>()
    //添加多个Realm
    realms.add(adminShiroRealm())
    realms.add(teacherShiroRealm())
    realms.add(studentShiroRealm())
    securityManager.setRealms(realms)
    // 自定义缓存实现 使用redis
    securityManager.setCacheManager(cacheManager())
    // 自定义session管理 使用redis
    securityManager.setSessionManager(sessionManager())
    //注入记住我管理器;
    securityManager.setRememberMeManager(rememberMeManager())
    return securityManager
    }

    /**
    * 系统自带的Realm管理,主要针对多realm
    * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
    //自己重写的ModularRealmAuthenticator
    UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator()
    modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy())
    return modularRealmAuthenticator
    }

    @Bean
    public AdminShiroRealm adminShiroRealm() {
    AdminShiroRealm adminShiroRealm = new AdminShiroRealm()
    adminShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
    return adminShiroRealm
    }

    @Bean
    public StudentShiroRealm studentShiroRealm() {
    StudentShiroRealm studentShiroRealm = new StudentShiroRealm()
    studentShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
    return studentShiroRealm
    }

    @Bean
    public TeacherShiroRealm teacherShiroRealm() {
    TeacherShiroRealm teacherShiroRealm = new TeacherShiroRealm()
    teacherShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher())//设置解密规则
    return teacherShiroRealm
    }

    //因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher()
    hashedCredentialsMatcher.setHashAlgorithmName("md5")//散列算法:这里使用MD5算法;
    hashedCredentialsMatcher.setHashIterations(2)//散列的次数,比如散列两次,相当于 md5(md5(""));
    return hashedCredentialsMatcher;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    接下来就是Controller中实现登录的功能了,这里我只贴出我项目中Admin登录的代码:

    package com.ciyou.edu.controller.admin

    import com.ciyou.edu.config.shiro.common.LoginType
    import com.ciyou.edu.config.shiro.common.UserToken
    import com.ciyou.edu.entity.Admin
    import com.ciyou.edu.utils.JSONUtil
    import org.apache.shiro.SecurityUtils
    import org.apache.shiro.authc.AuthenticationException
    import org.apache.shiro.subject.Subject
    import org.slf4j.Logger
    import org.slf4j.LoggerFactory
    import org.springframework.stereotype.Controller
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RequestMethod
    import org.springframework.web.bind.annotation.ResponseBody


    /**
    * @Author C.
    * @Date 2018-02-02 20:46
    * admin登录Controller
    */
    @Controller
    class AdminLoginController {

    private static final Logger logger = LoggerFactory.getLogger(AdminLoginController.class)
    private static final String ADMIN_LOGIN_TYPE = LoginType.ADMIN.toString()

    /**
    * admin登录
    * @param admin
    * @return 登录结果
    */
    @RequestMapping(value="/adminLogin",method=RequestMethod.POST, produces="application/json;charset=UTF-8")
    @ResponseBody
    public String loginAdmin(Admin admin){
    logger.info("登录Admin: " + admin)
    //后台校验提交的用户名和密码
    if(!admin?.getAdminName() || admin?.adminName?.trim() == ""){
    return JSONUtil.returnFailReuslt("账号不能为空")
    }else if(!admin?.getPassword() || admin?.getPassword()?.trim() == ""){
    return JSONUtil.returnFailReuslt("密码不能为空")
    }else if(admin?.getAdminName()?.length() < 3 || admin?.getAdminName()?.length() >15){
    return JSONUtil.returnFailReuslt("账号长度必须在3~15之间")
    }else if(admin?.getPassword()?.length() < 3 || admin?.getPassword()?.length() >15){
    return JSONUtil.returnFailReuslt("密码长度必须在3~15之间")
    }

    //获取Subject实例对象
    //在shiro里面所有的用户的会话信息都会由Shiro来进行控制,那么也就是说只要是与用户有关的一切的处理信息操作都可以通过Shiro取得,
    // 实际上可以取得的信息可以有用户名、主机名称等等,这所有的信息都可以通过Subject接口取得
    Subject subject = SecurityUtils.getSubject()

    //将用户名和密码封装到继承了UsernamePasswordToken的userToken
    UserToken userToken = new UserToken(admin?.getAdminName(), admin?.getPassword(), ADMIN_LOGIN_TYPE)
    userToken.setRememberMe(false)
    try {
    //认证
    // 传到ModularRealmAuthenticator类中,然后根据ADMIN_LOGIN_TYPE传到AdminShiroRealm的方法进行认证
    subject?.login(userToken)
    //Admin存入session
    SecurityUtils.getSubject()?.getSession()?.setAttribute("admin",(Admin)subject?.getPrincipal())
    return JSONUtil.returnSuccessResult("登录成功")
    } catch (AuthenticationException e) {
    //认证失败就会抛出AuthenticationException这个异常,就对异常进行相应的操作,这里的处理是抛出一个自定义异常ResultException
    //到时候我们抛出自定义异常ResultException,用户名或者密码错误
    logger.info("认证错误:" + e.getMessage())
    return JSONUtil.returnFailReuslt("账号或者密码错误")
    }
    }

    @RequestMapping(value="/admin/adminLogout")
    public String logoutAdmin(){
    SecurityUtils.getSubject()?.logout()
    return "redirect:/adminLogin"
    }


    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    现在Spring Boot中集成Shiro实现多realm配置就完成了。

    感谢以下博文,在我学习的过程中给了我很多帮助,我的博文也有一些内容参考他们的,还不够清楚的读者可以参考:
    shiro实现不同身份使用不同Realm进行验证
    SpringBoot+Shiro学习之数据库动态权限管理和Redis缓存
    Springboot多realm集成,无ini文件,无xml配置

    想看项目具体源码,或者对我项目感兴趣的可以查看:CIYOU

    点赞 7
    ————————————————
    版权声明:本文为CSDN博主「c.」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/cckevincyh/article/details/79629022

  • 相关阅读:
    如何解决Ubuntu下的的“system program problem detected”
    GDM, KDM, LightDM, SDDM显示管理器的区别和安装配置
    在Linux下安装使用vokoscreen录制网站上的视频与声音-Ubuntu-mate-20.04桌面环境测试录屏推荐
    Ubuntu-mate-20.04-desktop安装总结
    云上渗透-基于阿里云环境渗透
    fastadmin前台getshell漏洞
    gitlab CI/CD Custom SAST
    前端神器RRWEB
    【转】六种减小Docker镜像大小的方法
    splunk 多个数据关联查询
  • 原文地址:https://www.cnblogs.com/weizhxa/p/12364930.html
Copyright © 2020-2023  润新知