若依框架thymeleaf升级分布式--dora带你来探险

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
.cn 域名,1个 12个月
简介: 若依框架thymeleaf升级分布式

管理后台没有采用前后端分离,采用shiro+thymeleaf的若依框架,项目模块较多,项目分组较多,有希望进行分布式开发的需求,可以共同探讨下本文的思路。

升级思路

  • 编写通用的shiro认证模块,所有服务引入此模块。
  • 搭建认证中心portal,用户登录都走认证中心
  • 基于session的有效domain,所有服务使用相同域名, shiro通过redis存放session。

思路参见以前文章

分布式shiro权限验证 一

分布式shiro权限验证 二

通用的shiro认证模块

构建模块ruoyi-dora-starter-web ,其他web项目引入此starter即可引入框架shiro认证。

引入shiro依赖
<!-- shiro -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
编写shro配置

主要需注入 UserRealm credentialsMatcher SessionsSecurityManager

@Configuration
public class ShiroConfiguration {

  /**
  * 用户认证
  **/
  @Bean
  public UserRealm userRealm() {
    UserRealm userRealm = new UserRealm();
    // 设置密码认证器
    userRealm.setCredentialsMatcher(credentialsMatcher());
    return userRealm;
  }

  @Bean
  public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    // filterChain 过滤静态资源
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    chainDefinition.addPathDefinition("/static/**", "anon");
    chainDefinition.addPathDefinition("/ajax/**", "anon");
    chainDefinition.addPathDefinition("/css/**", "anon");
    chainDefinition.addPathDefinition("/file/**", "anon");
    chainDefinition.addPathDefinition("/fonts/**", "anon");
    chainDefinition.addPathDefinition("/html/**", "anon");
    chainDefinition.addPathDefinition("/i18n/**", "anon");
    chainDefinition.addPathDefinition("/img/**", "anon");
    chainDefinition.addPathDefinition("/js/**", "anon");
    chainDefinition.addPathDefinition("/ruoyi/**", "anon");
    chainDefinition.addPathDefinition("/login", "anon");
    chainDefinition.addPathDefinition("/captcha", "anon");
    chainDefinition.addPathDefinition("/logout", "anon");
    chainDefinition.addPathDefinition("/ruoyi.png", "anon");
    chainDefinition.addPathDefinition("/favicon.ico", "anon");
    chainDefinition.addPathDefinition("/layuiadmin/**", "anon");
    chainDefinition.addPathDefinition("/druid/**", "anon");
    chainDefinition.addPathDefinition("/api/**", "anon");
    chainDefinition.addPathDefinition("/**", "authc");
    return chainDefinition;
  }

  @Bean
  public HashedCredentialsMatcher credentialsMatcher() {
    // 密码认证器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // Md5Hash.ALGORITHM_NAME
    credentialsMatcher.setHashAlgorithmName("SHA-256");
    credentialsMatcher.setStoredCredentialsHexEncoded(false);
    credentialsMatcher.setHashIterations(1024);

    return credentialsMatcher;
  }

  @Bean
  public SessionsSecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm());

    return securityManager;
  }
认证授权 realm

为验证可行性,认证授权暂时写定

@Slf4j
public class UserRealm extends AuthorizingRealm {
​
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {
    // 角色 权限信息 暂时写定
    User user = (User) SecurityUtils.getSubject().getPrincipal();
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    Set<String> roles = new HashSet();
    Set<String> permissions = new HashSet();
    if ("admin".equals(user.getUserName())) {
      roles.add("admin");
      permissions.add("op:write");
    } else {
      roles.add("user");
      permissions.add("op:read");
    }
​
    authorizationInfo.setRoles(roles);
    authorizationInfo.setStringPermissions(permissions);
    return authorizationInfo;
  }
​
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {
    String username = (String)authenticationToken.getPrincipal();
    String credentials = new String((char[])authenticationToken.getCredentials());
    User user = new User();
    user.setUserName(username);
    String password = credentials;
    // 此处暂时跳过密码验证  与注入的bean credentialsMatcher算法一致,算出 hashedCredentials
    // 实际需从数据库中取出
    String salt = "salt";
    int hashIterations = 1024;
    String encodedPassword = (new SimpleHash("SHA-256", password, Util.bytes(salt), hashIterations)).toBase64();
    log.info("password: {}  encode: {}",password,encodedPassword);
    user.setPassword(encodedPassword);
    // authenticationToken.getCredentials() + salt 经credentialsMatcher加密  与 hashedCredentials 比较
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), Util.bytes(salt), this.getName());
    return authenticationInfo;
  }
Starter 自动配置

META-INF文件下spring-factories加入配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ruoyi.dora.web.config.DoraWebAutoConfiguration,\
com.ruoyi.dora.web.config.ShiroConfiguration

至此认证模块starter完成

搭建认证中心

构建独立项目 ruoyi-dora-portal-web

引入ruoyi-dora-starter-web
<dependency>
  <groupId>com.ruoyi</groupId>
  <artifactId>ruoyi-dora-starter-web</artifactId>
</dependency>
引入若依前端资源文件

引入若依admin前端的静态资源文件 static包、templates包所有文件

登录控制
@Slf4j
@Controller
public class LoginController {

  @GetMapping("/login")
  public String loginPage (Model model) {
    if(SecurityUtils.getSubject().isAuthenticated()){
      return "redirect:/index";
    }
    // 若依框架的配置 临时处理
    Map<String, Object> configMap = new HashMap<>();
    configMap.put("sys.account.registerUser", true);
    model.addAttribute("config", configMap);
    model.addAttribute("captchaEnabled", false);

    return "login";
  }

  @GetMapping("/logout")
  public String logout () {
    SecurityUtils.getSubject().logout();

    return "redirect:/login";
  }

  @PostMapping("/login")
  @ResponseBody
  public AjaxResult ajaxLogin (String username, String password, Boolean rememberMe) {
    UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
    Subject subject = SecurityUtils.getSubject();
    try {
      subject.login(token);
      return success();
    } catch (AuthenticationException e) {
      log.error("login error.",e);
      String msg = "用户或密码错误";
      if (StringUtils.isNotEmpty(e.getMessage())) {
        msg = e.getMessage();
      }
      return error(msg);
    }
  }


  @GetMapping("/unauth")
  public String unauth () {
    return "error/unauth";
  }

}
主页index

主要获取了数据库menu菜单,及UI风格设置。ISysMenuService 采用了mybatis-plus的generator生成器代码风格,menuService.selectMenusByUser 参见若依原代码,或本文gitee代码。 对一些代码做了临时处理,直接写定。

@Slf4j
@Controller
public class PortalController {

  @Autowired
  private ISysUserService sysUserService;

  @Autowired
  private ISysMenuService menuService;

  @Autowired
  private ISysConfigService configService;

  @GetMapping({"/","/index"})
  public String index(ModelMap modelMap){

    Object principal = SecurityUtils.getSubject().getPrincipal();
    log.info("principal {}",principal.toString());
    SysUser user = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, "admin"), false);
    List<SysMenu> menus = menuService.selectMenusByUser(user);
    modelMap.put("menus", menus);
    modelMap.put("user", user);
    modelMap.put("sideTheme", configService.selectConfigByKey("sys.index.sideTheme").getConfigValue());
    modelMap.put("skinName", configService.selectConfigByKey("sys.index.skinName").getConfigValue());
    modelMap.put("ignoreFooter", configService.selectConfigByKey("sys.index.ignoreFooter").getConfigValue());
    // 配置临时处理
//    modelMap.put("copyrightYear", RuoYiConfig.getCopyrightYear());
//    modelMap.put("demoEnabled", RuoYiConfig.isDemoEnabled());
//    modelMap.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
//    modelMap.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
    modelMap.put("copyrightYear", "2021");
    modelMap.put("demoEnabled", "true");
    modelMap.put("isDefaultModifyPwd", false);
    modelMap.put("isPasswordExpired", false);

    // 菜单导航显示风格
//    String menuStyle = configService.selectConfigByKey("sys.index.menuStyle");
    String menuStyle = "default";
    // 移动端,默认使左侧导航菜单,否则取默认配置
    String indexStyle = menuStyle;
    //ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")) ? "index" : menuStyle;

    // 优先Cookie配置导航菜单
//    Cookie[] cookies = ServletUtils.getRequest().getCookies();
//    for (Cookie cookie : cookies)
//    {
//      if (StringUtils.isNotEmpty(cookie.getName()) && "nav-style".equalsIgnoreCase(cookie.getName()))
//      {
//        indexStyle = cookie.getValue();
//        break;
//      }
//    }
    if("topnav".equalsIgnoreCase(indexStyle)){
      return "index-topnav";
    }
    return "index";
  }


  /**
  * UI 工作区域frame  main
  **/
  @GetMapping("/system/main")
  public String sysMain(Model model){
    model.addAttribute("version","0.0.1");
    return "main";
  }
}

至此启动项目,即可进行登录,并进入到index首页(菜单项具体功能并未实现,可参见若依原码)。

redis存放session

对于多端服务将session统一管理。

引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>

yaml 配置redis

启动类添加注解@EnableRedisHttpSession 将session存放在redis

spring:
  datasource:
    url: jdbc:p6spy:mysql://localhost:3306/ry?useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver

  #redis
  redis:
    host: localhost
    port: 6379

栗子sky-web

建立独立项目 demo-sky-web

引入上面的依赖ruoyi-dora-starter-web

<dependency>
  <groupId>com.ruoyi</groupId>
  <artifactId>ruoyi-dora-starter-web</artifactId>
</dependency>

将shrio的登录地址指向认证中心ruoyi-dora-portal-web的地址
配置reids, 启动类添加注解@EnableRedisHttpSession 将session存放在redis

shiro:
  loginUrl: http://localhost:8600/login

spring:
  redis:
    host: localhost
    port: 6379

编写简单测试页面

@Controller public class SkyController {

@GetMapping("/sky") public String skyPage(){ return "sky"; }

}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sky</title>
</head>
<body>
<h1>Sky sky</h1>
</body>
</html>

数据库添加菜单, 将菜单地址指向 demo-sky的地址全路径http://localhost:8080/sky要求域名相同(此处域名为localhost)

INSERT INTO `ry`.`sys_menu`(`menu_id`, `menu_name`, `parent_id`, `order_num`, `url`, `target`, `menu_type`, `visible`, `is_refresh`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (510, '天天业务管理', 5, 1, 'http://localhost:8080/sky', '', 'C', '0', '1', 'system:sky:view', 'fa fa-user-o', 'admin', '2021-07-01 02:17:28', '', NULL, '天天业务管理菜单');

直接访问 http://localhost:8080/sky> 跳转到登录页,登录后跳转到主页index,访问菜单天天业务可以正常访问。

image-20210723142848537.png

至此基于若依框架 shiro+thymeleaf的分布式项目可行性探索完成,后续工作ruoyi-dora-starter-web 中UserReaml 用户信息、角色、权限信息可以动态获取 如通过http、openfeign调用user服务获取。若依原框架功能未完全迁移,按需参见原框架。

总结

  • 利用session的domain作用域,使用相同域名,及redis统一管理session, 进行分布式session管理。
  • 抽象出一个公用start进行shiro的配置,业务模块引入start即可使用。
  • 所有模块的登录页面都指向统一的登录认证中心。

若对此方面感兴趣,可留言扣1,待功能完善后回复评论。或参见gitee代码ruoyi-dora共同探讨研究。

项目Gitee ruoyi-dora (dora 源自动画片--爱探险的朵拉)

若依是一个优秀的后台框架。

相关文章
|
26天前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
26天前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
2天前
|
分布式计算 大数据 数据处理
技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
随着大数据和人工智能技术的发展,数据处理的需求日益增长。阿里云推出的MaxCompute MaxFrame(简称“MaxFrame”)是一个专为Python开发者设计的分布式计算框架,它不仅支持Python编程接口,还能直接利用MaxCompute的云原生大数据计算资源和服务。本文将通过一系列最佳实践测评,探讨MaxFrame在分布式Pandas处理以及大语言模型数据处理场景中的表现,并分析其在实际工作中的应用潜力。
16 2
|
25天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
46 6
|
25天前
|
数据库
如何在Seata框架中配置分布式事务的隔离级别?
总的来说,配置分布式事务的隔离级别是实现分布式事务管理的重要环节之一,需要认真对待和仔细调整,以满足业务的需求和性能要求。你还可以进一步深入研究和实践 Seata 框架的配置和使用,以更好地应对各种分布式事务场景的挑战。
28 6
|
23天前
|
消息中间件 运维 数据库
Seata框架和其他分布式事务框架有什么区别
Seata框架和其他分布式事务框架有什么区别
23 1
|
1月前
|
机器学习/深度学习 自然语言处理 并行计算
DeepSpeed分布式训练框架深度学习指南
【11月更文挑战第6天】随着深度学习模型规模的日益增大,训练这些模型所需的计算资源和时间成本也随之增加。传统的单机训练方式已难以应对大规模模型的训练需求。
132 3
|
1月前
|
机器学习/深度学习 并行计算 Java
谈谈分布式训练框架DeepSpeed与Megatron
【11月更文挑战第3天】随着深度学习技术的不断发展,大规模模型的训练需求日益增长。为了应对这种需求,分布式训练框架应运而生,其中DeepSpeed和Megatron是两个备受瞩目的框架。本文将深入探讨这两个框架的背景、业务场景、优缺点、主要功能及底层实现逻辑,并提供一个基于Java语言的简单demo例子,帮助读者更好地理解这些技术。
79 2
|
2月前
|
分布式计算 Hadoop
Hadoop-27 ZooKeeper集群 集群配置启动 3台云服务器 myid集群 zoo.cfg多节点配置 分布式协调框架 Leader Follower Observer
Hadoop-27 ZooKeeper集群 集群配置启动 3台云服务器 myid集群 zoo.cfg多节点配置 分布式协调框架 Leader Follower Observer
52 1
|
2月前
|
存储 SQL 消息中间件
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
52 0
下一篇
DataWorks