zoukankan      html  css  js  c++  java
  • spring boot 2 + shiro 实现权限管理

    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;
        }
    }

    说明:
    这里使用@Getter,@Setter注解,不能使用@Data注解,因为实体使用了jpa的@oneToMany ,加载方式为lazy,在主表查询时关联表未加载,而主表使用@Data后会实现带关联表属性的hashCode和equals等方法。在运行过程中调用关联表数据时会显示异常 java.lang.stackoverflowerror。

    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

      2、访问/user/userAdd, Response body显示登录页

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

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

     Response body显示登录成功。

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

     

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

  • 相关阅读:
    easyui 动态列
    easyui editor combobox multiple
    千年老二
    Win7上Git安装及配置过程
    Tomcat自动部署
    Win7下安装git
    修改mysql默认字符集的方法
    oracle 查询当前库中所有表以及某表字段信息
    treegrid and datagrid ctrl or shift selectRow
    条件注释判断浏览器<!--[if !IE]><!--[if IE]><!--[if lt IE 6]><!--[if gte IE 6]>
  • 原文地址:https://www.cnblogs.com/gdjlc/p/12057612.html
Copyright © 2011-2022 走看看