[SpringBoot] SpringBoot + Shiro + Mybatis + Thymeleaf integration

In the SpringBoot project, the permission of the current login is controlled by integrating Shiro security framework.

The requirements are as follows:

A page index There are ADD and UPDATE links on HTML. Click the ADD link to jump to ADD HTML page; The same is true for UPDATE links. There are now two users: root and tom. Root user only has ADD access and operation permission, while tom user only has UPDATE access and operation permission.

As shown below:

The content of the page is as follows (without permission):

root login:

Of course, this ADD link can be clicked. After clicking it, ADD will be displayed HTML page content:

tom user login: login with root user. Only the page content is UPDATE

[development environment]:

  • IDEA-2020.2
  • SpringBoot-2.5.5
  • MAVEN-3.5.3
  • Shiro
  • Thymeleaf
  • Mybatis
  • Mysql
  • Druid

Project structure diagram

backstage:


Reception:

sql file

CREATE TABLE `tab_user` (
  `id` varchar(11) NOT NULL,
  `name` varchar(30) NOT NULL,
  `pwd` varchar(30) NOT NULL,
  `perms` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Entity class

public class User {
    private String id;
    private String name;
    private String pwd;
    private String perms;
    // getter/setter
}

1. Import dependency:

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

<!-- thymeleaf -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--spring-shiro-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-web-starter</artifactId>
  <version>1.4.0</version>
</dependency>
<!--mybatis-->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.0.0</version>
</dependency>
<!--mysql-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.38</version>
</dependency>
<!--druid-->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.20</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>
<!--shiro-thymeleaf-->
<dependency>
  <groupId>com.github.theborakompanioni</groupId>
  <artifactId>thymeleaf-extras-shiro</artifactId>
  <version>2.0.0</version>
</dependency>

2. Add application YML configuration:

spring:
  thymeleaf:
    cache: false
    prefix: classpath:/templates/  # Configuration templates (not necessary) are templates by default
    suffix: .html
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/zzc?useUnicode=true&characterEncoding=utf8
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource

    # SpringBoot does not inject the following attribute values by default
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    # Configure the filters for monitoring statistics interception. After removing the filters, the sql in the monitoring interface cannot be counted, and 'wall' is used for firewall
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

mybatis:
  type-aliases-package: com.zzc.pojo # Entity class alias
  mapper-locations: classpath:mapper/*.xml # mapper configuration file (required)
  configuration:
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # Log output location
    map-underscore-to-camel-case: true # Hump naming

#logging:
#  level:
#    com.zzc.mapper: debug # Set log level

3. Background routing controller

@Controller
public class RoutingController {

    @GetMapping({"/", "/index"})
    public String index(Model model) {
        model.addAttribute("msg", "Hello Shior");
        return "index";
    }

    @GetMapping("/user/add")
    public String add() {
        return "user/add";
    }

    @GetMapping("/user/update")
    public String update() {
        return "user/update";
    }

    @GetMapping("/toLogin")
    public String toLogin() {
        return "login";
    }

    @PostMapping("/login")
    public String login(String username, String pwd, Model model) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, pwd);
        try {
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "user name does not exist");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "Password error");
            return "login";
        }
    }

    @GetMapping("/noauth")
    @ResponseBody
    public String noauth() {
        return "This page is not authorized";
    }

}

4. Shiro configuration class ShiroConfig

Like spring security, using Shiro also requires adding a configuration class. Three components need to be configured in this configuration class:

  • ShiroFilterFactoryBean
  • DefaultWebSecurityManager
  • Custom Realm

The code is as follows:

@Configuration
public class ShiroConfig {

    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        // Associate DefaultWebSecurityManager
        filterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // Built in filter
        /**
         * anno:Access without authentication
         * authc:Authentication is required to access
         * user:I have to remember to visit
         * perms:You must have permission to access a resource
         * role:You must have a role permission to access
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        // Configure authorization
        filterMap.put("/user/add", "perms[user:add]");
        filterMap.put("/user/update", "perms[user:update]");
        
        /*filterMap.put("/user/add", "authc");
        filterMap.put("/user/update", "authc");*/
        filterMap.put("/user/*", "authc");
        filterFactoryBean.setFilterChainDefinitionMap(filterMap);

        // If you do not have permission to access, jump to the login page
        filterFactoryBean.setLoginUrl("/toLogin");
        // If the authorization is not successful, you will jump to the authorization page
        filterFactoryBean.setUnauthorizedUrl("/noauth");
        return filterFactoryBean;
    }

    @Bean(name = "defaultWebSecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // Associated Realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

	// Custom Realm
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

explain:

  1. getShiroFilterFactoryBean() method:
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");

This code indicates:

  • To access the url of / user/add, you need to have the add permission of user
  • To access the url of / user/update, you need to have the update permission of user

Format: perms[user:add] is the writing method of permission Operation in Shiro.

filterMap.put("/user/*", "authc");

This code indicates:

  • To access the url starting with user, you must authenticate

5. Custom Realm

public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // to grant authorization
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal();
        info.addStringPermission(currentUser.getPerms());
        return info;
    }

    // authentication
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
        // User name authentication
        String username = userToken.getUsername();
        User user = userService.queryUserByName(username);
        if (null == user) {
            // When null is returned, an exception UnknownAccountException will be thrown
            return null;
        }
   
        // Password authentication: Shiro does it for us. Can encrypt
        return new SimpleAuthenticationInfo(user, user.getPwd(), "");
    }
}

explain:

  1. In the authentication method doGetAuthenticationInfo(), we put the login successful user information into the first parameter in the SimpleAuthenticationInfo construction method, that is: return new SimpleAuthenticationInfo(user, user.getPwd(), ""); User in. Then, when authorizing, we can obtain user information. That is, in the authorization method doGetAuthorizationInfo(), we pass the code: subject subject subject = securityutils getSubject(); User currentUser = (User)subject. getPrincipal(); Get current login user

6. UserMapper interface

Since the code in UserService is similar to that in UserMapper, the code in UserService will be omitted and the UserMapper interface code will be posted directly:

@Repository
public interface UserMapper {
    User queryUserByName(String name);
}

The corresponding usermapper XML configuration file, as follows:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzc.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
        SELECT
            id,name,pwd,perms
        FROM TAB_USER
        WHERE 1=1
        AND name = #{name}
    </select>

</mapper>

Note: also configure Mapper scanning in the main method. As follows:

@MapperScan("com.zzc.mapper")
@SpringBootApplication
public class ShiroApplication {
    public static void main( String[] args ) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}

7. Home page index html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>home page</title>
</head>
<body>
<h1 >Hello Shiro</h1>
<p th:text="${msg}">12</p>
<p>
    <a th:href="@{/toLogin}">Sign in</a>
</p>
<hr>
<a th:href="@{/user/add}">ADD</a>
<a th:href="@{/user/update}">UPDATE</a>
</body>
</html>

8. Login page login html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Sign in</title>
</head>
<body>
<h1>Sign in</h1><hr>
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}" method="post">
    <table>
        <tr>
            <td>user name</td>
            <td>
                <input type="text" name="username" />
            </td>
        </tr>
        <tr>
            <td>password</td>
            <td>
                <input type="password" name="pwd" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Sign in" />
            </td>
        </tr>
    </table>
</form>
</body>
</html>

In fact, most of the requirements have been completed here. There is one problem left: the root user only has user:add permission, so it should not display user:update. Namely:

Moreover, when we click the UPDATE link, we will find that we do not have permission (this is normal):

Therefore, the front desk needs to be processed here! Only show the content that the current user has permission!!

Shiro needs to be integrated through Thymeleaf

9-1. Import dependency (imported above)

<dependency>
  <groupId>com.github.theborakompanioni</groupId>
  <artifactId>thymeleaf-extras-shiro</artifactId>
  <version>2.0.0</version>
</dependency>

9-2. Modify the configuration class ShiroConfig

Need to register a bean --- shirodialect

Add a Bean to the original configuration class:

// Shiro-Thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
    return new ShiroDialect();
}

9-3. Modify user realm

Only modify the method doGetAuthenticationInfo().

After the user logs in successfully, the user information is stored in the Session

// authentication
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
    // User name authentication
    String username = userToken.getUsername();
    User user = userService.queryUserByName(username);
    if (null == user) {
        // When null is returned, an exception UnknownAccountException will be thrown
        return null;
    }
    // After successful login, put the current login user into the Session
    Subject currentSubject = SecurityUtils.getSubject();
    Session session = currentSubject.getSession();
    session.setAttribute("user", user);

    // Password authentication: Shiro does it for us. Can encrypt
    return new SimpleAuthenticationInfo(user, user.getPwd(), "");
}

9-4. Modify the homepage index html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>home page</title>
</head>
<body>
<h1 >Hello Shiro</h1>
<p th:text="${msg}">12</p>
<div th:if="${session.user == null}">
    <p>
        <a th:href="@{/toLogin}">Sign in</a>
    </p>
</div>
<hr>
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">ADD</a>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">UPDATE</a>
</div>
</body>
</html>

Well, SpringBoot integration Shiro is here~~

Source address

Keywords: Java Spring Boot intellij-idea

Added by Stickdragon on Sun, 06 Feb 2022 07:23:13 +0200