SpringBoot integrates Shiro to realize authority management

Like you who love programming!
Learn SpringBoot actual combat course https://edu.csdn.net/course/detail/31433
Learn SpringCloud introductory course https://edu.csdn.net/course/detail/31451


Overview

System security is currently a problem that will be considered for large and small software projects. Here we will get to know the Shiro framework of Apache and understand how to use it to implement permission management.

RBAC authority management

Question: In the enterprise OA system, should the department heads and ordinary employees see the same menu and operation functions in the system?

The responsibilities of employees are different in the enterprise. After logging in to the software system, different users must not have the same data operation and query authority. Otherwise, for the enterprise, internal management and security will have problems.

RBAC (Role Based Access Control) authority management, role-based access control.

Main target:

1) User

2) Role

3) Permission

After different users log in to the system, they have different roles, and different roles can perform different operations on system resources.

Users and roles are in a many-to-many relationship, and roles and permissions are in a many-to-many relationship.

[External link image transfer failed. The origin site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-kNPg2k16-1608710040661)(shiro.assets/1608601616339.png)]

Shiro

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password and session management. Using Shiro's easy-to-understand API, you can quickly and easily obtain any application, from the smallest mobile application to the largest web and enterprise application.

Main API

  • SecurityManager Security Manager, complete core business

  • Subject provides methods for developers to call

  • Realm provides user login and authorization data

  • SecurityUtils tool class, used to integrate other components

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-XvrbMz9a-1608710040663)(shiro.assets/9825bc315c6034a8f93c7d0cce13495408237665.jpg)]

Shiro

1. Add dependency

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

2. Add shiro.ini file under resources, the content is as follows

[users]
zhang=123,role1,role2
wang=123,role2

[roles]
role1=user:create,user:update
role2=user:create,user:delete
role3=user:create

shiro.ini is Shiro's configuration file, [users] is user configuration, the format is: account = password, role 1, role 2...

[roles] is the permission configuration, the format is: role = permission 1, permission 2...

3. Test

//创建基于Ini文件的安全管理器工厂
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//获得安全管理器
SecurityManager instance = factory.getInstance();
//配置安全管理器
SecurityUtils.setSecurityManager(instance);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("wang", "123");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否登录成功:" + subject.isAuthenticated());
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

The effect of logging in with wang:

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-vI0Dd27c-1608710040665)(shiro.assets/1608602846457.png)]

The effect of logging in with zhang:

Insert picture description here

A wrong password will throw an exception:

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-qhOU7LSv-1608710040670)(shiro.assets/1608602919980.png)]

Custom Realm

In the above case, the ini file is used to configure users, passwords, roles, and permissions. It is too simple and not suitable for enterprise-level projects. The important data of users, roles, and permissions in the real project are all stored in the database, and we need to write by ourselves The classes and methods implement login and authorization.

The AuthorizingRealm class implements the Realm interface and provides two methods:

  • doGetAuthenticationInfo returns login authentication information, which is called after the subject executes the login method
  • doGetAuthorizationInfo returns user authorization information. This method is called when the subject makes authority judgment

1) Define the Realm class. In this case, for simplicity, the account password is fixed zhang, 123, the role role1 is added, and the permissions user: select, user: insert, and user: delete are added.

/**
 * 用户Realm
 */
public class MyRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获得登录用户
        String username = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("登录用户授权:" + username);
        //授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //添加角色
        info.addRole("role1");
        //添加权限
        info.addStringPermission("user:select");
        info.addStringPermission("user:insert");
        info.addStringPermission("user:delete");
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获得用户输入的账号
        String username = authenticationToken.getPrincipal().toString();
        if(!username.equals("zhang")){
            throw new UnknownAccountException("此用户不存在");
        }
        //返回验证信息,参数:1、用户名 2、正确密码 3、realm名称
        return new SimpleAuthenticationInfo(username,"123", getName());
    }
}

2) Use custom Realm for login and authorization

//创建默认安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//安全管理器配置自定义Realm
securityManager.setRealm(new MyRealm());
//SecurityUtils配置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("zhang", "123");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

Password encryption and salt

User passwords are generally not saved in plain text, so security cannot be guaranteed, so encryption is generally required.

The SimpleHash class can implement basic encryption, with several creation methods:

new SimpleHash("加密算法","原始密码")
new SimpleHash("加密算法","原始密码",盐)
new SimpleHash("加密算法","原始密码",盐,迭代次数)

Parameter Description:

The encryption algorithm generally uses the commonly used md5 algorithm

The role of salt is to improve password security. If the original passwords of two users are both 123, the encrypted ciphertext is the same. If one user’s password is cracked, the other user’s password is also cracked. Add salt to the password. Each user's salt is different, and the password will be different after encryption, which increases the difficulty of cracking.

The number of iterations is to encrypt the ciphertext again after encrypting it once, which can also improve security.

Below we use the md5 algorithm to encrypt "123456", the salt is "007", and the number of iterations is 10.

SimpleHash md5 = new SimpleHash("md5", "123456",ByteSource.Util.bytes("007"), 10);
System.out.println(md5);
输出:44202d045439dc33a2e43d2828d08e19

Modify the doGetAuthenticationInfo method of MyRealm, where the ciphertext and salt are directly written in the code. In actual application, the ciphertext and salt are queried from the database by the user name.

//返回验证信息,参数:1、用户名 2、正确密码 3、盐 4、realm名称
return new SimpleAuthenticationInfo(username,"44202d045439dc33a2e43d2828d08e19", ByteSource.Util.bytes("007"),getName());

Need to add a password matcher to the custom Realm

//创建默认安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//安全管理器配置自定义Realm
MyRealm realm = new MyRealm();
//创建密码匹配器
HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
//设置迭代次数
md5.setHashIterations(10);
//配置匹配器
realm.setCredentialsMatcher(md5);
securityManager.setRealm(realm);
//SecurityUtils配置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("zhang", "123456");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否登录成功:" + subject.isAuthenticated());
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-VzpaSypf-1608710040672)(shiro.assets/1608618247076.png)]

SpringBoot + MyBatis + Shiro matching

1. Table design

[External link image transfer failed. The origin site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-XS3cRTkB-1608710040674)(shiro.assets/1608618965879.png)]

  • s_user user table

  • s_role role table

  • s_menu menu table (permission table)

  • s_user_menu User role intermediate table

  • s_role_menu intermediate table of role menu

2. Add dependency

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
</dependency>

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.7.0</version>
</dependency>

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

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

2. SpringBoot configuration

# jdbc配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/erp_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
# mybatis配置
mybatis-plus.type-aliases-package=com.blb.blb_erp.entity
mybatis-plus.mapper-locations=classpath:mapper/*.xml
# shiro配置
# 登录页面
shiro.loginUrl=/pages/login.html
# 登录失败跳转页面
shiro.unauthorizedUrl=/pages/failed.html
# 登录成功跳转页面
shiro.successUrl=/pages/index.html

3. Write Mapper interface

Three methods are required:

  1. Find users by username
  2. Query all menus by user id
  3. Query all roles by user id
/**
 *  用户接口
 */
public interface SUserMapper extends BaseMapper<SMenu>{

    /**
     * 通过用户名查询用户
     * @param username
     * @return
     */
    SUser selectUserByUsername(String username);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SUserMapper">

    <select id="selectUserByUsername" resultType="SUser">
        select * from s_user where user_name = #{username}
    </select>
</mapper>
/**
 *  菜单接口
 */
public interface SMenuMapper extends BaseMapper<SMenu>{
    /**
     * 根据userId查询所有权限
     * @param userId
     * @return
     */
    List<SMenu> selectMenusByUserId(String userId);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SMenuMapper">

    <select id="selectMenusByUserId" resultType="SMenu">
        select m.* from s_user u,s_role r,s_user_role ur,s_menu m,s_role_menu rm
        where ur.role_id = r.id and ur.user_id = u.id and rm.role_id = r.id and rm.menu_id = m.id
        and u.id = #{userId}
    </select>
</mapper>
/**
 *  角色接口
 */
public interface SRoleMapper extends BaseMapper<SMenu>{

    /**
     * 根据用户id查询所有角色
     * @param userId
     * @return
     */
    List<SRole> selectRolesByUserId(String userId);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SRoleMapper">

    <select id="selectRolesByUserId" resultType="SRole">
        select r.* from s_user_role ur
        join s_user u on ur.user_id = u.id
        join s_role r on ur.role_id = r.id
        where  ur.user_id = #{userId}
    </select>
</mapper>

4. Custom Realm

/**
 * 用户Realm
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private SUserMapper userMapper;
    @Autowired
    private SRoleMapper roleMapper;
    @Autowired
    private SMenuMapper menuMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获得用户对象
        SUser user = (SUser) principalCollection.getPrimaryPrincipal();
        //查询权限和角色
        List<SMenu> menus = menuMapper.selectMenusByUserId(user.getId());
        List<SRole> roles = roleMapper.selectRolesByUserId(user.getId());
        //保存权限和角色名称的集合
        List<String> strRoles = new ArrayList<>();
        roles.forEach(r -> strRoles.add(r.getRoleName()));
        List<String> strMenus = new ArrayList<>();
        menus.forEach(m -> strMenus.add(m.getMenuName()));
        //返回带有角色和权限名称的授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(strRoles);
        info.addStringPermissions(strMenus);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获得账号
        String username = authenticationToken.getPrincipal().toString();
        //通过账号查询用户
        SUser user = userMapper.selectUserByUsername(username);
        if(user == null){
            throw new UnknownAccountException("此用户不存在");
        }
        //返回验证信息  参数:1、用户对象 2、正确密码 3、盐 4、realm名称
        return new SimpleAuthenticationInfo(user,user.getPassWord(), ByteSource.Util.bytes(user.getSalt()),getName());
    }
}

5. Shiro configuration class

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

    //返回Realm
    @Bean
    public UserRealm myRealm(){
        UserRealm myRealm = new UserRealm();
        //设置密码匹配器
        HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
        md5.setHashIterations(10);
        myRealm.setCredentialsMatcher(md5);
        //关闭缓存
        myRealm.setCachingEnabled(false);
        return myRealm;
    }

    //返回面向Web开发的安全管理器
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
        //设置自定义Realm
        sm.setRealm(myRealm());
        return sm;
    }

    //返回Shiro过滤器链定义
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        //定义过滤器链key为url,value为anon不验证,authc验证
        //anon在前,authc在后,需要使用LinkedHashMap保留顺序
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("/pages/login.html","anon");
        map.put("/user/login","anon");
        map.put("/**","authc");
        chainDefinition.addPathDefinitions(map);
        return chainDefinition;
    }

    //启动thymeleaf的shiro标签
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

}

6. Startup

@MapperScan("com.blb.blb_erp.mapper")
@SpringBootApplication
public class BlbErpApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlbErpApplication.class, args);
    }

}

7. Controller

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonResult {
    private int code;
    private Object data;

}
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/login")
    public JsonResult login(String username,String password){
        //创建Token
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //获得subject
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return new JsonResult(1,"登录成功");
        }catch (AuthenticationException ex){
            ex.printStackTrace();
        }
        return new JsonResult(0,"账号或密码错误");
    }
    
    @RequiresRoles("管理员")
    @GetMapping("/role-admin")
    public String testRole(){
        return "有管理员角色";
    }

    @RequiresPermissions("部门管理")
    @GetMapping("/menu-dept")
    public String testMenu(){
        return "有部门管理权限";
    }
}

@RequiresRoles and @RequiresPermissions are written on the method of the controller, and the logged-in user has the corresponding role and permission to access.

RememberMe

You can add the remember me function on the login page. After you check it, you will directly enter the system without logging in next time.

1. Add the RememberMe checkbox on the page

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" href="/elementui/index.css">
    <style>
        .box-card{
            margin:200px auto;
            width: 480px;
        }
        .clearfix{
            text-align: center;
            color:#303133;
            font-size: 18px;
        }
        .login-form{
            width: 400px;
        }
    </style>
</head>
<body>
<div id="app">
    <el-card class="box-card">
        <div slot="header" class="clearfix">
            <span>系统登录</span>
        </div>
       <el-form class="login-form" ref="form"  :model="form" label-width="80px">
            <el-form-item label="账号">
           <el-input v-model="form.username"></el-input>
           </el-form-item>
           <el-form-item label="密码">
               <el-input type="password" v-model="form.password"></el-input>
           </el-form-item>
           <el-form-item >
               <el-checkbox v-model="form.rememberMe">记住我</el-checkbox>
           </el-form-item>
           <el-form-item>
               <el-button type="primary" @click="login">登 录</el-button>
               <el-button>取 消</el-button>
           </el-form-item>
        </el-form>
    </el-card>
</div>
<script src="/vue/vue.js"></script>
<script src="/elementui/index.js"></script>
<script src="/axios/axios.min.js"></script>
<script src="/qs/qs.min.js"></script>
<script>
    new Vue({
       el:"#app",
        data:{
           form:{username:"",password:"",rememberMe:false}
        },
        methods:{
           login:function () {
               //Qs.stringify(this.form) 将form由{xx:值} 转为 xx=值&xx=值
               axios.post("/user/login",Qs.stringify(this.form))
                   .then(res=>{
                      if(res.data.code == 1){
                          location.href = "/pages/index.html";
                      }
                   });
           }
        }
    });
</script>
</body>
</html>

2. Modify the login method

@PostMapping("/login")
public JsonResult login(String username,String password,Boolean rememberMe){
    //创建Token
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //设置记住我
    token.setRememberMe(rememberMe);
    //获得subject
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
        return new JsonResult(1,"登录成功");
    }catch (AuthenticationException ex){
        ex.printStackTrace();
    }
    return new JsonResult(0,"账号或密码错误");
}

3. Modify Shiro configuration class

//创建RememberMe管理器
public CookieRememberMeManager rememberMeManager(){
    CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
    SimpleCookie rememberMe = new SimpleCookie("rememberMe");
    //单位是秒 过期时间
    rememberMe.setMaxAge(60 * 10);
    rememberMeManager.setCookie(rememberMe);
    return rememberMeManager;
}

//返回面向Web开发的安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
    DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    //设置自定义Realm
    sm.setRealm(myRealm());
    //设置RememberMe
    sm.setRememberMeManager(rememberMeManager());
    return sm;
}

//返回Shiro过滤器链定义
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    ...
    //这是使用user,和authc的区别是:authc必须经过验证,user经过验证或RememberMe都可以登录
    map.put("/**","user");
    chainDefinition.addPathDefinitions(map);
    return chainDefinition;
}

[External link image transfer...(img-k6KRxRh4-1608710040676)]

Choose Remember me to log in to the system, close the browser and open the system page again, you do not need to log in to directly enter the system.

End

If this article is helpful to you, please like it :)


If you need to learn other Java knowledge, poke here ultra-detailed knowledge of Java Summary

Guess you like

Origin blog.csdn.net/u013343114/article/details/111592137