SpringBoot integration Shiro (full version)

EDITORIAL: From the end of 2018 to start learning SpringBoot, also wrote a number of projects with SpringBoot. This concludes the record about some of the knowledge and learning Springboot. If you are learning SpringBoot, I can focus, learn together and progress together.


Before writing project basic security controls are used in SpringSecurity, later found by Shiro in the actual development is also very much. Here with a look at the basic usage Shiro in Springboot of it! For SpringSecurity of venue: SpringSecurity Security Controls Guide .

Shiro 简介

1 Introduction

Apache Shiro is a Java-security (permissions) framework. Shiro can very easily develop enough good applications, it not only can be used in JavaSE environment, can also be used in a JavaEE environment. Shiro can be done: authentication, authorization, encryption, session management, integration with Web, caching. Relative to SpringSecurity simpler, less complex nor SpringSecurity.

2, shiro architecture

Here Insert Picture Description
Shiro three functional modules

  1. Subject: body, generally refers to a user (the operation to SecurityManager).
  2. SecurityManager: Security Manager, to manage all Subject, can be used with internal security components (association Realm).
  3. Realms: used authority to verify the information, the link bridge shiro data.

Segments

  1. Authentication: authentication / login, verify that the user has the appropriate identity (account password to verify).
  2. Authorization: Authorization, verify that a user has authenticated a privilege.
  3. Session Manager: session management, user login, user information stored in the session session.
  4. Cryptography: encryption to protect data security, such as encrypted passwords stored in the database, not stored in plain text.
  5. Web Support: Web support, integrated Web environment.
  6. Caching: cache, user information, roles, permissions, etc., etc. redis such as cache to cache.
  7. Concurrency: multithreading to verify, open another thread in a thread, can automatically propagate permissions past.
  8. Testing: Testing Support;
  9. Run As: allows a user pretending to be access to another user (if they are allowed) identity.
  10. Remember Me: Remember me login, then do not come back next time logged.

Database Design

1, table relationships

Here Insert Picture Description
All Menu (TbMenu) =====> to be displayed on the page

Role (SysRole) =====> corresponding roles and role menu

Users (SysUser) =====> corresponding user and user roles

Users and Roles middle of the table (sys_user_role) ====> Users and Roles middle of the table
[Note] rights management implementation principles are similar, although not the same security controls used, but the same basic design of the database table, where design and database tables SpringSecurity dynamic menu control rights are basically the same.

2, the database table structure

Menu table tb_menu
Here Insert Picture Description
roles and permissions menu table sys_role, where a parent node parent is null for the role, not null corresponding role menu privileges.
Here Insert Picture Description
User table sys_user.
Here Insert Picture Description
Many relationships and user roles, user roles and the intermediate table sys_user_role (automatically generated by Spring-Data-Jpa).
Here Insert Picture Description

Building project

1, the new project Springboot

SprintBoot create a project, add a dependency needs of the project (persistence layer used here is SpringDataJpa).
Shiro dependence

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>

pom.xml complete dependence Code

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.mcy</groupId>
	<artifactId>springboot-shiro</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-shiro</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- shiro与spring整合依赖 -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

2, project structure

Here Insert Picture Description

3, attributes the configuration database link

spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

server.port=80
server.servlet.context-path=/shiro

Write code

1, the preparation of an entity class

Table menu entity class TbMenu, Spring-Data-Jpa entity classes according to a new database or table structure corresponding to the update, details can be accessed Spring-Data-Jpa entry

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 菜单表
 * @author
 */
@Entity
public class TbMenu {
    private Integer id;
    private String name;
    private String url;
    private Integer idx;
    @JsonIgnore
    private TbMenu parent;
    @JsonIgnore
    private List<TbMenu> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(unique = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }

    @ManyToOne
    @CreatedBy
    public TbMenu getParent() {
        return parent;
    }

    public void setParent(TbMenu parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    @OrderBy(value = "idx")
    public List<TbMenu> getChildren() {
        return children;
    }

    public void setChildren(List<TbMenu> children) {
        this.children = children;
    }
}

Roles and permissions table SysRole, parent is null for the role, not null permissions.

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
/***
 * 角色及角色对应的菜单权限
 * @author
 *parent 为null时为角色,不为null时为权限
 */
public class SysRole {
    private Integer id;
    private String name;    //名称
    @JsonIgnore
    private SysRole parent;
    private Integer idx;    //排序
    @JsonIgnore
    private List<SysRole> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToOne
    @CreatedBy
    public SysRole getParent() {
        return parent;
    }

    public void setParent(SysRole parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    public List<SysRole> getChildren() {
        return children;
    }

    public void setChildren(List<SysRole> children) {
        this.children = children;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }
}

Finally managed to achieve is that users only need to assign the corresponding roles for users to add on it, when the user logs in, permission to display the corresponding role.

import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 用户表
 */
@Entity
public class SysUser {
    private Integer id;
    private String username;    //账号
    private String password;    //密码
    private String name;        //姓名
    @JsonIgnore
    private List<SysRole> roles = new ArrayList<>();    //角色

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(length = 20, unique = true)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(length = 100)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToMany(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
    @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
}

2, Shiro configuration class

Shiro configuration class ShiroConfig.

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @description Shiro配置类
 */
@Configuration
public class ShiroConfig {

    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次数
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean("userRealm")
    public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(matcher);
        return userRealm;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置 SecurityManager,安全管理器
        bean.setSecurityManager(securityManager);

        // 设置登录跳转页面
        bean.setLoginUrl("/index");
        // 设置未授权提示页面(没有权限访问后的页面)
        bean.setUnauthorizedUrl("/unAuth");
        /**
         * Shiro内置过滤器,可以实现拦截器相关的拦截器
         *    常用的过滤器:
         *      anon:无需认证(登录)可以访问
         *      authc:必须认证才可以访问
         *      user:如果使用rememberMe的功能可以直接访问
         *      perms:该资源必须得到资源权限才可以访问
         *      role:该资源必须得到角色权限才可以访问
         **/
        Map<String, String> filterMap = new LinkedHashMap<>();

        filterMap.put("/login","anon");
        filterMap.put("/user","authc");
        filterMap.put("/system","perms[system]");
        filterMap.put("/static/**","anon");

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

        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

    /**
     * 注入 securityManager
     */
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联realm.
        securityManager.setRealm(userRealm(hashedCredentialsMatcher));
        return securityManager;
    }
}

Custom Realm to query the user's role and permissions information and save it to the permissions manager. code show as below:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService userService;
    
    /**
     * 授权逻辑方法
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行授权");

        //获取当前登录对象
        Subject subject = SecurityUtils.getSubject();
        SysUser user = (SysUser)subject.getPrincipal();
        if(user != null){
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 角色与权限字符串集合
            Collection<String> rolesCollection = new HashSet<>();

            //获得当前用户的角色
            List<SysRole> roles = user.getRoles();
            for(SysRole role : roles){
                rolesCollection.add(role.getName());
            }
            //添加当前用户的角色权限,用于判断可以访问那些功能
            info.addStringPermissions(rolesCollection);
            return info;
        }
        return null;
    }

    /**
     * 认证
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        System.out.println("执行认证");

        // 编写shiro判断逻辑,判断用户名和密码
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        // 判断用户名
        SysUser bean = userService.findByUsername(token.getUsername());

        //用户名不存在
        if(bean == null){
            throw new UnknownAccountException();
        }

        //以账号作为加密的盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getUsername());

        // 判断密码,
        return new SimpleAuthenticationInfo(bean, bean.getPassword(),
                credentialsSalt, getName());
    }
}

Wherein the password is encrypted using the MD5 encryption, encrypted code, as follows:

public static void main(String[] args){
    String hashAlgorithName = "MD5";
    //加密密码
    String password = "123456";
    //加密次数
    int hashIterations = 1024;
    //账号作为加密的盐值
    ByteSource credentialsSalt = ByteSource.Util.bytes("admin");
    Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
    System.out.println(obj);
}

3, write controller

New IndexController test controller. Here persistence framework using SpringDataJpa, query simply follow the naming convention to query method, so here directly write controller code. To learn more about SpringDataJpa junior partner, the venue: the Spring-JPA the Data-entry.
IndexController controller code as follows:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysRoleService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {

    @Autowired
    private SysRoleService roleService;

    @RequestMapping({"/index", "/"})
    public String index(){
        return "login";
    }

    @RequestMapping(value = "/login")
    public String login(String username, String password, Model model){
        //1.获取subject
        Subject subject = SecurityUtils.getSubject();

        //2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            //3.执行登录方法
            subject.login(token);
            //登录成功
            return "redirect:/mian";
        } catch (UnknownAccountException e) {
            //e.printStackTrace();
            //登录失败:用户名不存在
            model.addAttribute("msg", "用户名不存在");
            return "login";
        } catch (IncorrectCredentialsException e) {
            //e.printStackTrace();
            //登录失败:用户名不存在
            model.addAttribute("msg", "密码输入有误");
            return "login";
        }
    }

    @RequestMapping(value = "/mian")
    public String main(){
        SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
        System.out.println(user.getName());
        //登录成功后,输出对应的角色和菜单
        for(SysRole role: user.getRoles()){
            System.out.println(role.getName()+"=====角色");
            for(SysRole roles : role.getChildren()){
                System.out.println(roles.getName()+"===="+role.getName()+"角色对应的菜单");
            }
        }
        return "main";
    }

    @RequestMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }
        return "redirect:/main";
    }

    @RequestMapping("/unAuth")
    public String unAuth(){
        return "unAuth";
    }

    @RequestMapping("/system")
    public String system(){
        return "system";
    }
    
    @RequestMapping("/user")
    public String user(){
        return "user";
    }
}

4, write the page

Page content are relatively simple, mainly a login page, page after a successful login and access to several pages.
Login.html login page code is as follows (written in relatively simple test page, the page font tags for failed login prompt, backstage pass over the content):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <style type="text/css">
        #main{
            width: 400px;
            margin: 100px auto;
        }
        input{
            border-radius: 5px;
            width: 200px;
            height: 20px;
        }
        button{
            margin-left: 30px;
            width: 80px;
            border-radius: 15px;
        }
    </style>
</head>
<body>
    <div id="main">
        <form action="login" method="post">
            <font color="red" th:text="${msg}"></font><br><br>
            账号:<input type="text" name="username"><br><br>
            密码:<input type="password" name="password"><br><br>
            <button type="submit">登录</button>
        </form>
    </div>
</body>
</html>

Main.html successful login page (the page jump several hyperlinks)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <li><a href="user">登录即可访问</a></li>
    <li><a href="system">管理员权限才能访问</a></li>
    <li><a href="logout">退出登录</a></li> 
</body>
</html>

Administrator rights to access the page system.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    管理员权限,才能访问
</body>
</html>

Log in to access the page user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登录后即可访问
</body>
</html>

You do not have permission to access the page, prompt page unAuth.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>您暂时没有权限访问该页面</p>
</body>
</html>

test

1, application testing

After running the project visit http: // localhost / shiro / index , enter the login page, login user with Administrator privileges.
Here Insert Picture Description
After login is successful, it can be seen from the Editor console first step in a Shrio certification. After performing the method jumps back page after a successful login, the output of the corresponding roles and role corresponding menu.
[Note] This case is not in the foreground menu traversal function, menus, and other rights did not write the corresponding CRUD. Add data directly in the database for testing. If you want to know the function of the realization traverse menus, menu permissions, visit SpringSecurity dynamic menu load . In which the database design, functional operation is the same.

Click on "administrative privileges to access the" First would be to perform authorization, whether there is an administrator at the discretion, determine whether you can access. Because the user has logged in as an administrator, so you can access the effect is as follows:
Here Insert Picture Description
the content is the system.html page. If there is no administrator, the display effect is as follows:
Here Insert Picture Description

2, case download

Download Case Code: https://github.com/machaoyin/springboot-shrio

Finally, what shortcomings, welcome to point out, looking forward to communicate with you.

Published 122 original articles · won praise 442 · views 160 000 +

Guess you like

Origin blog.csdn.net/qq_40205116/article/details/104946528