欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

spring boot 2 + shiro 实现权限管理

程序员文章站 2022-07-04 22:39:09
Shiro是一个功能强大且易于使用的Java安全框架,主要功能有身份验证、授权、加密和会话管理。 ......

shiro是一个功能强大且易于使用的java安全框架,主要功能有身份验证、授权、加密和会话管理。
看了网上一些文章,下面2篇文章写得不错。
springboot2.0 集成shiro权限管理 
spring boot:整合shiro权限框架 

自己动手敲了下代码,在第一篇文章上加入了第二篇文章的swagger测试,另外自己加入lombok简化实体类代码,一些地方代码也稍微修改了下,过程中也碰到一些问题,最终代码成功运行。

开发版本:
intellij idea 2019.2.2
jdk1.8
spring boot 2.1.11
mysql8.0

一、创建springboot项目,添加依赖包和配置application.yml
在idea中创建一个新的springboot项目

1、pom.xml引用的依赖包如下:

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-web</artifactid>
        </dependency>

        <dependency>
            <groupid>org.apache.shiro</groupid>
            <artifactid>shiro-spring</artifactid>
            <version>1.4.2</version>
        </dependency>

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-data-jpa</artifactid>
        </dependency>

        <dependency>
            <groupid>mysql</groupid>
            <artifactid>mysql-connector-java</artifactid>
        </dependency>

        <dependency>
            <groupid>org.projectlombok</groupid>
            <artifactid>lombok</artifactid>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupid>io.springfox</groupid>
            <artifactid>springfox-swagger2</artifactid>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupid>io.springfox</groupid>
            <artifactid>springfox-swagger-ui</artifactid>
            <version>2.9.2</version>
        </dependency>

2、application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.driver
    url: jdbc:mysql://localhost:3306/testdb?usessl=false&servertimezone=utc
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建
      naming:
        physical-strategy: org.hibernate.boot.model.naming.physicalnamingstrategystandardimpl #按字段名字建表
        #implicit-strategy: org.hibernate.boot.model.naming.implicitnamingstrategylegacyjpaimpl #驼峰自动映射为下划线格式
    show-sql: true # 默认false,在日志里显示执行的sql语句
    database: mysql
    database-platform: org.hibernate.dialect.mysql5innodbdialect

二、创建实体类

创建user、role、permission三个实体类,根据规则会自动生成两个中间表,最终数据库有5个表。
另外添加一个model处理登录结果。

1、user

package com.example.shiro.entity;

import lombok.getter;
import lombok.setter;
import org.springframework.format.annotation.datetimeformat;
import javax.persistence.*;
import java.time.localdate;
import java.time.localdatetime;
import java.util.list;

@entity
@getter
@setter
public class user {
    @id
    @generatedvalue(strategy = generationtype.auto)
    private long userid;

    @column(nullable = false, unique = true)
    private string username; //登录用户名

    @column(nullable = false)
    private string name;//名称(昵称或者真实姓名,根据实际情况定义)

    @column(nullable = false)
    private string password;

    private string salt;//加密密码的盐

    private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.

    @manytomany(fetch= fetchtype.eager)//立即从数据库中进行加载数据;
    @jointable(name = "userrole", joincolumns = { @joincolumn(name = "userid") }, inversejoincolumns ={@joincolumn(name = "roleid") })
    private list<role> rolelist;// 一个用户具有多个角色

    @datetimeformat(pattern = "yyyy-mm-dd hh:mm")
    private localdatetime createtime;//创建时间

    @datetimeformat(pattern = "yyyy-mm-dd")
    private localdate expireddate;//过期日期

    private string email;

    /**密码盐. 重新对盐重新进行了定义,用户名+salt,这样就不容易被破解 */
    public string getcredentialssalt(){
        return this.username+this.salt;
    }
}

说明:
实体使用了jpa的@onetomany 和lombok的@data,在运行过程中调用关联表数据时会显示异常 java.lang.*error。
因为使用@onetomany默认配置,所以加载方式为lazy。在主表查询时关联表未加载,而主表使用@data后会实现带关联表属性的hashcode和equals等方法。
所以这里不使用@data注解,而是用@getter,@setter注解。

2、role

package com.example.shiro.entity;

import lombok.getter;
import lombok.setter;

import javax.persistence.*;
import java.util.list;

@entity
@getter
@setter
public class role {
    @id
    @generatedvalue(strategy = generationtype.auto)
    private long roleid; // 编号

    @column(nullable = false, unique = true)
    private string role; // 角色标识程序中判断使用,如"admin",这个是唯一的:

    private string description; // 角色描述,ui界面显示使用

    private boolean available = boolean.true; // 是否可用,如果不可用将不会添加给用户

    //角色 -- 权限关系:多对多关系;
    @manytomany(fetch= fetchtype.eager)
    @jointable(name="rolepermission",joincolumns={@joincolumn(name="roleid")},inversejoincolumns={@joincolumn(name="permissionid")})
    private list<permission> permissions;

    // 用户 - 角色关系定义;
    @manytomany
    @jointable(name="userrole",joincolumns={@joincolumn(name="roleid")},inversejoincolumns={@joincolumn(name="userid")})
    private list<user> users;// 一个角色对应多个用户
}

3、permission

package com.example.shiro.entity;

import lombok.getter;
import lombok.setter;

import javax.persistence.*;
import java.util.list;

@entity
@getter
@setter
public class permission {
    @id
    @generatedvalue(strategy = generationtype.auto)
    private long permissionid;//主键.

    @column(nullable = false)
    private string permissionname;//名称.

    @column(columndefinition="enum('menu','button')")
    private string resourcetype;//资源类型,[menu|button]

    private string url;//资源路径.

    private string permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view

    private long parentid; //父编号

    private string parentids; //父编号列表

    private boolean available = boolean.true;

    //角色 -- 权限关系:多对多关系;
    @manytomany(fetch= fetchtype.eager)
    @jointable(name="rolepermission",joincolumns={@joincolumn(name="permissionid")},inversejoincolumns={@joincolumn(name="roleid")})
    private list<role> roles;
}

4、loginresult

package com.example.shiro.model;

import lombok.data;

@data
public class loginresult {
    private boolean islogin = false;
    private string result;
}

三、dao

1、添加一个dao基础接口:baserepository

package com.example.shiro.repository;

import org.springframework.data.jpa.repository.jpaspecificationexecutor;
import org.springframework.data.repository.norepositorybean;
import org.springframework.data.repository.pagingandsortingrepository;

import java.io.serializable;

@norepositorybean
public interface baserepository<t, i extends serializable> extends pagingandsortingrepository<t, i>, jpaspecificationexecutor<t> {
}

2、userrepository

package com.example.shiro.repository;

import com.example.shiro.entity.user;

public interface userrepository extends baserepository<user,long> {
    user findbyusername(string username);
}

四、service

1、loginservice

package com.example.shiro.service;

import com.example.shiro.model.loginresult;

public interface loginservice {

    loginresult login(string username, string password);

    void logout();
}

2、userservice

package com.example.shiro.service;

import com.example.shiro.entity.user;

public interface userservice {
    user findbyusername(string username);
}

五、service.impl

1、loginserviceimpl

package com.example.shiro.service.impl;

import com.example.shiro.model.loginresult;
import com.example.shiro.repository.userrepository;
import com.example.shiro.service.loginservice;
import org.apache.shiro.securityutils;
import org.apache.shiro.authc.authenticationexception;
import org.apache.shiro.authc.incorrectcredentialsexception;
import org.apache.shiro.authc.unknownaccountexception;
import org.apache.shiro.authc.usernamepasswordtoken;
import org.apache.shiro.session.session;
import org.apache.shiro.subject.subject;
import org.springframework.stereotype.service;

@service
public class loginserviceimpl implements loginservice {

    @override
    public loginresult login(string username, string password) {
        loginresult loginresult = new loginresult();
        if (username == null || username.isempty()) {
            loginresult.setlogin(false);
            loginresult.setresult("用户名为空");
            return loginresult;
        }
        string msg = "";
        // 1、获取subject实例对象
        subject currentuser = securityutils.getsubject();

//        // 2、判断当前用户是否登录
//        if (currentuser.isauthenticated() == false) {
//
//        }

        // 3、将用户名和密码封装到usernamepasswordtoken
        usernamepasswordtoken token = new usernamepasswordtoken(username, password);

        // 4、认证
        try {
            currentuser.login(token);// 传到myauthorizingrealm类中的方法进行认证
            session session = currentuser.getsession();
            session.setattribute("username", username);
            loginresult.setlogin(true);
            return loginresult;
            //return "/index";
        } catch (unknownaccountexception e) {
            e.printstacktrace();
            msg = "unknownaccountexception -- > 账号不存在:";
        } catch (incorrectcredentialsexception e) {
            msg = "incorrectcredentialsexception -- > 密码不正确:";
        } catch (authenticationexception e) {
            e.printstacktrace();
            msg = "用户验证失败";
        }

        loginresult.setlogin(false);
        loginresult.setresult(msg);

        return loginresult;
    }

    @override
    public void logout() {
        subject subject = securityutils.getsubject();
        subject.logout();
    }
}

2、userserviceimpl

package com.example.shiro.service.impl;

import com.example.shiro.entity.user;
import com.example.shiro.repository.userrepository;
import com.example.shiro.service.userservice;
import org.springframework.stereotype.service;

import javax.annotation.resource;

@service
public class userserviceimpl implements userservice {
    @resource
    private userrepository userrepository;
    @override
    public user findbyusername(string username) {
        return userrepository.findbyusername(username);
    }
}

六、config配置类

1、创建realm

package com.example.shiro.config;

import com.example.shiro.entity.permission;
import com.example.shiro.entity.role;
import com.example.shiro.entity.user;
import com.example.shiro.service.userservice;
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.util.bytesource;

import javax.annotation.resource;

public class myshirorealm extends authorizingrealm {
    @resource
    private userservice userservice;

    /**
     * 身份认证:验证用户输入的账号和密码是否正确。
     * */
    @override
    protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {
        //获取用户输入的账号
        string username = (string) token.getprincipal();
        //通过username从数据库中查找 user对象.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        user user = userservice.findbyusername(username);
        if (user == null) {
            return null;
        }
        simpleauthenticationinfo authenticationinfo = new simpleauthenticationinfo(
                user,//这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限
                user.getpassword(),//密码
                bytesource.util.bytes(user.getcredentialssalt()),//salt=username+salt
                getname()//realm name
        );
        return authenticationinfo;
    }

    /**
     * 权限信息
     * */
    @override
    protected authorizationinfo dogetauthorizationinfo(principalcollection principals) {
        simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo();
        //如果身份认证的时候没有传入user对象,这里只能取到username
        //也就是simpleauthenticationinfo构造的时候第一个参数传递需要user对象
        user user  = (user)principals.getprimaryprincipal();
        for(role role : user.getrolelist()){
            //添加角色
            authorizationinfo.addrole(role.getrole());
            for(permission p:role.getpermissions()){
                //添加权限
                authorizationinfo.addstringpermission(p.getpermission());
            }
        }
        return authorizationinfo;
    }

}

2、配置shiro

package com.example.shiro.config;

import org.apache.shiro.authc.credential.hashedcredentialsmatcher;
import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor;
import org.apache.shiro.spring.web.shirofilterfactorybean;
import org.apache.shiro.web.mgt.defaultwebsecuritymanager;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.web.servlet.handler.simplemappingexceptionresolver;

import java.util.hashmap;
import java.util.map;
import java.util.properties;

@configuration
public class shiroconfig {
    //将自己的验证方式加入容器
    @bean
    myshirorealm myshirorealm() {
        myshirorealm myshirorealm = new myshirorealm();
        myshirorealm.setcredentialsmatcher(hashedcredentialsmatcher());
        return myshirorealm;
    }

    //权限管理,配置主要是realm的管理认证
    @bean
    defaultwebsecuritymanager securitymanager() {
        defaultwebsecuritymanager manager = new defaultwebsecuritymanager();
        manager.setrealm(myshirorealm());
        return manager;
    }

    //凭证匹配器(密码校验交给shiro的simpleauthenticationinfo进行处理)
    @bean
    public hashedcredentialsmatcher hashedcredentialsmatcher(){
        hashedcredentialsmatcher hashedcredentialsmatcher = new hashedcredentialsmatcher();
        hashedcredentialsmatcher.sethashalgorithmname("md5");//散列算法:这里使用md5算法;
        hashedcredentialsmatcher.sethashiterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedcredentialsmatcher;
    }

    // filter工厂,设置对应的过滤条件和跳转条件
    @bean
    shirofilterfactorybean shirofilterfactorybean() {
        shirofilterfactorybean bean = new shirofilterfactorybean();
        bean.setsecuritymanager(securitymanager());
        map<string, string> filtermap = new hashmap<string, string>();
        // 登出
        filtermap.put("/logout", "logout");
        // swagger
        filtermap.put("/swagger**/**", "anon");
        filtermap.put("/webjars/**", "anon");
        filtermap.put("/v2/**", "anon");
        // 对所有用户认证
        filtermap.put("/**", "authc");
        // 登录
        bean.setloginurl("/login");
        // 首页
        bean.setsuccessurl("/index");
        // 未授权页面,认证不通过跳转
        bean.setunauthorizedurl("/403");
        bean.setfilterchaindefinitionmap(filtermap);      
        return bean;
    }

    //开启shiro aop注解支持.
    @bean
    public authorizationattributesourceadvisor authorizationattributesourceadvisor(){
        authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor();
        authorizationattributesourceadvisor.setsecuritymanager(securitymanager());
        return authorizationattributesourceadvisor;
    }

    //shiro注解模式下,登录失败或者是没有权限都是抛出异常,并且默认的没有对异常做处理,配置一个异常处理
    @bean(name="simplemappingexceptionresolver")
    public simplemappingexceptionresolver
    createsimplemappingexceptionresolver() {
        simplemappingexceptionresolver r = new simplemappingexceptionresolver();
        properties mappings = new properties();
        mappings.setproperty("databaseexception", "databaseerror");//数据库异常处理
        mappings.setproperty("unauthorizedexception","/403");
        r.setexceptionmappings(mappings);  // none by default
        r.setdefaulterrorview("error");    // no default
        r.setexceptionattribute("exception");     // default is "exception"
        return r;
    }
}

3、配置swagger

package com.example.shiro.config;

import io.swagger.annotations.apioperation;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import springfox.documentation.builders.apiinfobuilder;
import springfox.documentation.builders.pathselectors;
import springfox.documentation.builders.requesthandlerselectors;
import springfox.documentation.service.apiinfo;
import springfox.documentation.service.contact;
import springfox.documentation.spi.documentationtype;
import springfox.documentation.spring.web.plugins.docket;
import springfox.documentation.swagger2.annotations.enableswagger2;

@configuration
@enableswagger2
public class swaggerconfig {
    @bean
    public docket api() {
        return new docket(documentationtype.swagger_2)
                .apiinfo(apiinfo())
                .select()
                .apis(requesthandlerselectors.any())
                .paths(pathselectors.any()).build();
    }
    private static apiinfo apiinfo() {
        return new apiinfobuilder()
                .title("api文档")
                .description("swagger api 文档")
                .version("1.0")
                .contact(new contact("name..", "url..", "email.."))
                .build();
    }
}

七、controller

1、logincontroller用来处理登录

package com.example.shiro.controller;

import com.example.shiro.entity.user;
import com.example.shiro.model.loginresult;
import com.example.shiro.service.loginservice;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.requestbody;
import org.springframework.web.bind.annotation.restcontroller;

import javax.annotation.resource;

@restcontroller
public class logincontroller {
    @resource
    private loginservice loginservice;

    @getmapping(value = "/login")
    public string login() {
        return "登录页";
    }

    @postmapping(value = "/login")
    public string login(@requestbody user user) {
        system.out.println("login()");
        string username = user.getusername();
        string password = user.getpassword();
        loginresult loginresult = loginservice.login(username,password);
        if(loginresult.islogin()){
            return "登录成功";
        } else {
            return "登录失败:" + loginresult.getresult();
        }
    }

    @getmapping(value = "/index")
    public string index() {
        return "主页";
    }

    @getmapping(value = "/logout")
    public string logout() {
        return "退出";
    }

    @getmapping("/403")
    public string unauthorizedrole(){
        return "没有权限";
    }
}

2、usercontroller用来测试访问,权限全部采用注解的方式。

package com.example.shiro.controller;

import org.apache.shiro.authz.annotation.requirespermissions;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.restcontroller;

@restcontroller
@requestmapping("/user")
public class usercontroller {
    //用户查询
    @getmapping("/userlist")
    @requirespermissions("user:view")//权限管理;
    public string userinfo(){
        return "userlist";
    }

    //用户添加
    @getmapping("/useradd")
    @requirespermissions("user:add")//权限管理;
    public string userinfoadd(){
        return "useradd";
    }

    //用户删除
    @getmapping("/userdel")
    @requirespermissions("user:del")//权限管理;
    public string userdel(){
        return "userdel";
    }
}

八、数据库预设一些数据

先运行一遍程序,jpa生成数据库表后,手工执行sql脚本插入样本数据。
用户admin的密码是123456

insert into `user` (`userid`,`username`,`name`,`password`,`salt`,`state`)
values ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1);

insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
values (1,1,'用户管理',0,'0/','user:view','menu','user/userlist');
insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
values (2,1,'用户添加',1,'0/1','user:add','button','user/useradd');
insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
values (3,1,'用户删除',1,'0/1','user:del','button','user/userdel');

insert into `role` (`roleid`,`available`,`description`,`role`) values (1,1,'管理员','admin');

insert into `rolepermission` (`permissionid`,`roleid`) values (1,1);
insert into `rolepermission` (`permissionid`,`roleid`) values (2,1);

insert into `userrole` (`roleid`,`userid`) values (1,1);

九、swagger测试

 1、启动项目,访问http://localhost:8080/swagger-ui.html

spring boot 2 + shiro 实现权限管理

 

 2、访问/user/useradd,系统返回到登录页

spring boot 2 + shiro 实现权限管理

 

 

3、访问post的/login,请求参数输入:

{
"username": "admin",
"password": "123456"
}

spring boot 2 + shiro 实现权限管理

 

 response body显示登录成功。

spring boot 2 + shiro 实现权限管理

 

 4、再次访问/user/useradd,因为登录成功了并且有权限,这次response body显示useradd

 spring boot 2 + shiro 实现权限管理

 

 5、访问/user/userdel,因为数据库没有配置权限,所以response body显示没有权限

spring boot 2 + shiro 实现权限管理