This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

Zoe is an excellent backend framework.

Instead of separating the front and back ends, shiro+ Thymeleaf’s hierarchical framework is adopted in the management background. There are many modules and groups of projects, which are expected to meet the needs of distributed development. We can discuss the ideas in this paper together.

Upgrade ideas

  • Write a generic Shiro authentication module into which all services are imported.
  • The authentication center portal is set up. Users log in to the authentication center
  • Session-based valid domain. All services use the same domain name. Shiro stores sessions through Redis.

See the previous article for ideas

Distributed Shiro permission verification 1

Distributed Shiro permission authentication II

Generic Shiro authentication module

Building module Ruoyi-dora-starter – Web, which other Web projects can introduce shiro authentication framework by introducing this starter.

Introduce Shiro dependencies
<! -- shiro -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
Copy the code
Write the SHro configuration

The main injection is the UserRealm credentialsMatcher SessionsSecurityManager

@Configuration
public class ShiroConfiguration {

  /** * User authentication **/
  @Bean
  public UserRealm userRealm(a) {
    UserRealm userRealm = new UserRealm();
    // Set password authenticator
    userRealm.setCredentialsMatcher(credentialsMatcher());
    return userRealm;
  }

  @Bean
  public ShiroFilterChainDefinition shiroFilterChainDefinition(a) {
    // filterChain Filters static resources
    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(a) {
    // Password authenticator
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // Md5Hash.ALGORITHM_NAME
    credentialsMatcher.setHashAlgorithmName("SHA-256");
    credentialsMatcher.setStoredCredentialsHexEncoded(false);
    credentialsMatcher.setHashIterations(1024);

    return credentialsMatcher;
  }

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

    return securityManager;
  }
Copy the code
Authentication and Authorization realm

To verify the feasibility, the authentication authorization is written temporarily

@Slf4j
public class UserRealm extends AuthorizingRealm {
​
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {
    // Role permission information is temporarily written
    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;
    // Password-authentication is the same as the injected Bean credentialsMatcher algorithm, which calculates hashedCredentials
    // The actual need to fetch from the database
    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 by compared with hashedCredentials credentialsMatcher encryption
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), Util.bytes(salt), this.getName());
    return authenticationInfo;
  }
Copy the code
Starter Automatic configuration

Add the configuration class “spring-Factories” to the meta-INF file

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

The starter authentication module is complete

Set up a certification center

Build the separate project Ruoyi-dora-portal-Web

The introduction ofruoyi-dora-starter-web
<dependency>
  <groupId>com.ruoyi</groupId>
  <artifactId>ruoyi-dora-starter-web</artifactId>
</dependency>
Copy the code
Import if the front – end resource file

Templates package and static resource file

The login control
@Slf4j
@Controller
public class LoginController {

  @GetMapping("/login")
  public String loginPage (Model model) {
    if(SecurityUtils.getSubject().isAuthenticated()){
      return "redirect:/index";
    }
    // Temporary processing depending on the configuration of the framework
    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 (a) {
    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 = "Incorrect user or password";
      if (StringUtils.isNotEmpty(e.getMessage())) {
        msg = e.getMessage();
      }
      returnerror(msg); }}@GetMapping("/unauth")
  public String unauth (a) {
    return "error/unauth"; }}Copy the code
Home page index

Main access to the database menu menu, and UI style Settings. ISysMenuService adopted mybatis – plus the generator generator code style, menuService. SelectMenusByUser see if, in accordance with the original code or gitee code in this paper. Some code to do temporary processing, write directly.

@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());
    // Configure temporary processing
// 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);

    // Menu navigation display style
// String menuStyle = configService.selectConfigByKey("sys.index.menuStyle");
    String menuStyle = "default";
    // On mobile, the left navigation menu is set by default; otherwise, the default configuration is set
    String indexStyle = menuStyle;
    //ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")) ? "index" : menuStyle;

    // Prioritize Cookie configuration navigation menu
// 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 work area frame Main **/
  @GetMapping("/system/main")
  public String sysMain(Model model){
    model.addAttribute("version"."0.0.1");
    return "main"; }}Copy the code

At this point start the project, you can log in, and enter the index home page (menu item specific function has not been implemented, see if according to the original code).

Redis store session

This section describes how to manage sessions for multiple services in a unified manner.

Introduction of depend on

<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>
Copy the code

Yaml configuration redis

Add @enableredisHttpSession to the startup class and store the session in 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
Copy the code

Chestnut sky – web

Set up a separate project, demo-sky-web

Introduce the above dependency ruoyi-dora-starter-web

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

Configure reIDS by pointing the login address of shrio to the address of ruoyi-dora-Portal-web authentication authority. Add the @enableredisHttpSession annotation to the startup class and store the session in Redis

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

spring:
  redis:
    host: localhost
    port: 6379
Copy the code

Write a simple test page

@Controller public class SkyController {

@GetMapping("/sky") public String skyPage(a){ return "sky"; }}Copy the code
<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sky</title>
</head>
<body>
<h1>Sky sky</h1>
</body>
</html>
Copy the code

Database add menu, the menu will address address the full path to the demo – sky http://localhost:8080/sky, require domain name is the same (domain name for the 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.'Everyday Business Management'.5.1.'http://localhost:8080/sky'.' '.'C'.'0'.'1'.'system:sky:view'.'fa fa-user-o'.'admin'.'the 2021-07-01 02:17:28'.' '.NULL.'Daily Business Management Menu');
Copy the code

Direct access to jump to the login page, http://localhost:8600/login, http://localhost:8080/sky homepage to jump to the index after login, to access the menu every day activities can be normal access.

So far, the feasibility exploration of distributed project based on Shiro + Thymeleaf has been completed. In Ruoyi – Dora-starter -web, UserReaml user information, roles, and permissions can be obtained dynamically, such as through HTTP or Openfeign calling user services. If the functions of the original framework are not completely migrated, see the original framework as required.

conclusion

  • Use the domain scope of the session, use the same domain name and Redis to centrally manage sessions and implement distributed session management.
  • Abstract a public start for shirO configuration, the service module can be used to introduce start.
  • The login page for all modules points to the unified login authentication authority.

If you are interested in this aspect, you can leave a message to button 1, and reply after the function is complete. Or see the Gitee code ruoyi-Dora for a study together.

Project Gitee Ruoyi – DORA