SpringBoot integra Shiro para realizar la gestión de autoridad

¡Como tú que amas la programación!
Aprenda los cursos prácticos de SpringBoot https://edu.csdn.net/course/detail/31433
Aprenda los cursos de introducción a SpringCloud https://edu.csdn.net/course/detail/31451


Visión general

La seguridad del sistema es actualmente un problema que será considerado para proyectos de software grandes y pequeños, aquí conoceremos el framework Shiro de Apache y entenderemos cómo usarlo para implementar la gestión de permisos.

Gestión de autoridad RBAC

Pregunta: En el sistema de OA empresarial, ¿deberían los jefes de departamento y los empleados ordinarios ver el mismo menú y las mismas funciones de operación en el sistema?

Las responsabilidades de los empleados en una empresa son diferentes. Después de iniciar sesión en el sistema de software, los diferentes usuarios no deben tener la misma autoridad para operar y consultar datos. De lo contrario, habrá problemas con la gestión interna y la seguridad de la empresa.

Gestión de autoridad RBAC (Control de acceso basado en roles), control de acceso basado en roles.

Objetivo principal:

1) Usuario

2) Rol

3) Permiso

Después de que diferentes usuarios inician sesión en el sistema, tienen diferentes roles y diferentes roles pueden realizar diferentes operaciones en los recursos del sistema.

Los usuarios y los roles están en una relación de muchos a muchos, y los roles y permisos están en una relación de muchos a muchos.

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuelas. Se recomienda guardar la imagen y subirla directamente (img-kNPg2k16-1608710040661) (shiro.assets / 1608601616339.png)]

Shiro

Apache Shiro es un marco de seguridad de Java potente y fácil de usar que realiza autenticación, autorización, contraseña y administración de sesiones. Con la API fácil de entender de Shiro, puede obtener rápida y fácilmente cualquier aplicación, desde la aplicación móvil más pequeña hasta la aplicación web y empresarial más grande.

API principal

  • SecurityManager Security Manager, negocio principal completo

  • El tema proporciona métodos para que los desarrolladores llamen

  • Realm proporciona datos de inicio de sesión y autorización del usuario

  • Clase de herramienta SecurityUtils, utilizada para integrar otros componentes

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuela. Se recomienda guardar la imagen y subirla directamente (img-XvrbMz9a-1608710040663) (shiro.assets / 9825bc315c6034a8f93c7d0cce13495408237665.jpg)]

Shiro

1. Agregar dependencia

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

2. Agregue el archivo shiro.ini en recursos, el contenido es el siguiente

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

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

shiro.ini es el archivo de configuración de Shiro, [usuarios] es la configuración del usuario, el formato es: cuenta = contraseña, rol 1, rol 2 ...

[roles] es la configuración de permisos, el formato es: rol = permiso 1, permiso 2 ...

3. Prueba

//创建基于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"));

El efecto de iniciar sesión con wang:

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuela. Se recomienda guardar la imagen y subirla directamente (img-vI0Dd27c-1608710040665) (shiro.assets / 1608602846457.png)]

El efecto de iniciar sesión con zhang:

Inserte la descripción de la imagen aquí

Una contraseña incorrecta generará una excepción:

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuela. Se recomienda guardar la imagen y subirla directamente (img-qhOU7LSv-1608710040670) (shiro.assets / 1608602919980.png)]

Reino personalizado

En el caso anterior, el archivo ini se usa para configurar usuarios, contraseñas, roles y permisos. Es demasiado simple y no es adecuado para proyectos de nivel empresarial. Los datos importantes de usuarios, roles y permisos en el proyecto real son todos almacenado en la base de datos, y necesitamos escribir por nosotros mismos. Las clases y métodos implementan el inicio de sesión y la autorización.

La clase AuthorizingRealm implementa la interfaz Realm y proporciona dos métodos:

  • doGetAuthenticationInfo devuelve información de autenticación de inicio de sesión, que se llama después de que el sujeto ejecuta el método de inicio de sesión
  • doGetAuthorizationInfo devuelve información de autorización del usuario, este método se llama cuando el sujeto hace un juicio de autoridad

1) Defina la clase Realm En este caso, por simplicidad, la contraseña de la cuenta es fija zhang, 123, se agrega el rol role1 y se agregan los permisos usuario: seleccionar, usuario: insertar y usuario: eliminar.

/**
 * 用户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) Utilice Reino personalizado para iniciar sesión y autorizar

//创建默认安全管理器
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"));

Cifrado de contraseña y sal

Las contraseñas de usuario generalmente no se guardan en texto sin formato, por lo que no se puede garantizar la seguridad, por lo que generalmente se requiere cifrado.

La clase SimpleHash puede implementar un cifrado básico, con varios métodos de creación:

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

Descripción de parámetros:

El algoritmo de cifrado generalmente utiliza el algoritmo md5 de uso común.

La función de salt es mejorar la seguridad de las contraseñas. Si las contraseñas originales de dos usuarios son 123, el texto cifrado es el mismo. Si se descifra la contraseña de un usuario, también se descifra la contraseña del otro usuario. Agregue salt a la contraseña. La sal del usuario es diferente y la contraseña será diferente después del cifrado, lo que aumenta la dificultad de descifrar.

El número de iteraciones es volver a cifrar el texto cifrado después de cifrarlo una vez, lo que también puede mejorar la seguridad.

A continuación, usamos el algoritmo md5 para cifrar "123456", la sal es "007" y el número de iteraciones es 10.

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

Modifique el método doGetAuthenticationInfo de MyRealm, donde el texto cifrado y la sal se escriben directamente en el código. En la aplicación real, el texto cifrado y la sal se consultan desde la base de datos por el nombre de usuario.

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

Necesita agregar un comparador de contraseñas al Reino personalizado

//创建默认安全管理器
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"));

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuelas. Se recomienda guardar la imagen y subirla directamente (img-VzpaSypf-1608710040672) (shiro.assets / 1608618247076.png)]

SpringBoot + MyBatis + Shiro a juego

1. Diseño de mesa

[Error al transferir la imagen del enlace externo. El sitio de origen puede tener un mecanismo de enlace anti-sanguijuela. Se recomienda guardar la imagen y subirla directamente (img-XS3cRTkB-1608710040674) (shiro.assets / 1608618965879.png)]

  • tabla de usuarios s_user

  • tabla de roles s_role

  • tabla de menú s_menu (tabla de permisos)

  • s_user_menu Tabla intermedia de roles de usuario

  • s_role_menu tabla intermedia del menú de funciones

2. Agregar dependencia

<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. Configuración de SpringBoot

# 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. Escriba la interfaz de Mapper

Se requieren tres métodos:

  1. Buscar usuarios por nombre de usuario
  2. Consultar todos los menús por identificación de usuario
  3. Consultar todos los roles por ID de usuario
/**
 *  用户接口
 */
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. Reino personalizado

/**
 * 用户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. Clase de configuración Shiro

/**
 * 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. Inicio

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

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

}

7. Controlador

@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, @RequiresPermissions se escriben en el método del controlador, y el usuario que ha iniciado sesión tiene el rol y el permiso de acceso correspondientes.

Recuérdame

Puede agregar la función Recordarme en la página de inicio de sesión. Después de marcarla, ingresará directamente al sistema sin iniciar sesión la próxima vez.

1. Agregue la casilla de verificación RememberMe en la página

<!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. Modifique el método de inicio de sesión

@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. Modificar la clase de configuración de Shiro

//创建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;
}

[Transferencia de imagen de enlace externo ... (img-k6KRxRh4-1608710040676)]

Elija Recordarme para iniciar sesión en el sistema, cierre el navegador y abra la página del sistema nuevamente, no necesita iniciar sesión para ingresar directamente al sistema.

Fin

Si este artículo te resulta útil, dale me gusta :)


Si necesita aprender otros conocimientos de Java, haga clic aquí Conocimiento ultra detallado de Java Resumen

Supongo que te gusta

Origin blog.csdn.net/u013343114/article/details/111592137
Recomendado
Clasificación