zoukankan      html  css  js  c++  java
  • SpringBoot2.x.x + Shiro + Redis 前后端分离实现

    Shiro架构图与基本知识

    1、Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。所以我这里也是简单介绍一下shiro的使用。

    2、非常简单;其基本功能点如下图所示:
    在这里插入图片描述
    Authentication:身份认证/登录,验证用户是不是拥有相应的身份;

    Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

    Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

    Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

    Web Support:Web支持,可以非常容易的集成到Web环境;

    Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

    Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

    Testing:提供测试支持;

    Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

    Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

    记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

    源码地址

    https://github.com/MRLEILOVE/spring-boot-shiro

    数据库结构

    5张表,也就是现在流行的权限设计模型RBAC,建表SQL已放在项目中。

    用户、角色、权限、用户-角色、角色-权限,关系如下。
    在这里插入图片描述

    使用的主要框架

    • SpringBoot 2.1.6.RELEASE
    • shiro-spring:1.4.0
    • druid数据库连接池:1.0.29
    • mybatis-plus:3.1.1
    • shiro-redis:3.1.0

    项目结构

    下面是整个项目结构,主要类已做注释。

    ├─main
    │ ├─java
    │ │ └─com
    │ │ └─leigq
    │ │ └─www
    │ │ └─shiro
    │ │ │ SpringBootShiroApplication.java
    │ │ │
    │ │ ├─bean
    │ │ │ CacheUser.java — 缓存用户信息
    │ │ │ Response.java — 统一返回结果
    │ │ │
    │ │ ├─config
    │ │ │ DruidDataSourceConfig.java — Druid数据源配置
    │ │ │ DruidMonitorConfig.java — Druid监控配置
    │ │ │ MyBatisPlusConfig.java — MyBatisPlus配置
    │ │ │ MySessionManager.java — 自定义session管理
    │ │ │ MyShiroRealm.java — 自定义 shiroRealm, 主要是重写其认证、授权
    │ │ │ ShiroConfig.java — Shiro管理
    │ │ │
    │ │ ├─controller
    │ │ │ LoginController.java
    │ │ │ PermissionController.java
    │ │ │ RoleController.java
    │ │ │ RolePermissionController.java
    │ │ │ UserController.java
    │ │ │ UserRoleController.java
    │ │ │
    │ │ ├─domain
    │ │ │ ├─entity
    │ │ │ │ Permission.java
    │ │ │ │ Role.java
    │ │ │ │ RolePermission.java
    │ │ │ │ User.java
    │ │ │ │ UserRole.java
    │ │ │ │
    │ │ │ └─mapper
    │ │ │ PermissionMapper.java
    │ │ │ RoleMapper.java
    │ │ │ RolePermissionMapper.java
    │ │ │ UserMapper.java
    │ │ │ UserRoleMapper.java
    │ │ │
    │ │ ├─service
    │ │ │ │ IPermissionService.java
    │ │ │ │ IRolePermissionService.java
    │ │ │ │ IRoleService.java
    │ │ │ │ IUserRoleService.java
    │ │ │ │ IUserService.java
    │ │ │ │
    │ │ │ └─impl
    │ │ │ PermissionServiceImpl.java
    │ │ │ RolePermissionServiceImpl.java
    │ │ │ RoleServiceImpl.java
    │ │ │ UserRoleServiceImpl.java
    │ │ │ UserServiceImpl.java
    │ │ │
    │ │ ├─util
    │ │ │ CodeGeneratorUtils.java — MyBatisPlus代码生成器
    │ │ │
    │ │ └─web
    │ │ │ GlobalExceptionHand.java — 全局异常处理
    │ │ │
    │ │ └─exception
    │ │ LoginException.java
    │ │
    │ └─resources
    │ │ application.yml
    │ │
    │ ├─config
    │ │ application-dev.yml
    │ │ application-prod.yml
    │ │ application-test.yml
    │ │
    │ ├─mappers
    │ │ PermissionMapper.xml
    │ │ RoleMapper.xml
    │ │ RolePermissionMapper.xml
    │ │ UserMapper.xml
    │ │ UserRoleMapper.xml
    │ │
    │ ├─sql
    │ │ shiro-V1.0.0.sql
    │ │ shiro-V1.0.1.sql — 最新版SQL
    │ │
    │ ├─static
    │ └─templates
    └─test
    └─java
    └─com
    └─leigq
    └─www
    └─shiro
    ├─base
    │ BaseApplicationTests.java

    └─test
    ShiroApplicationTests.java

    详细搭建过程

    建议直接将代码拉下来对照着文档看

    1、将最新版SQL导入数据库,SQL我已经放入项目中

    2、引入依赖。

    <?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 http://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.1.6.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.leigq.www</groupId>
        <artifactId>spring-boot-shiro</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>spring-boot-shiro</name>
        <description>shiro demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
            <druid.version>1.0.29</druid.version>
            <commons-collections4.version>4.1</commons-collections4.version>
            <mybatis-plus.version>3.1.1</mybatis-plus.version>
            <shiro-spring.version>1.4.0</shiro-spring.version>
            <shiro-redis.version>3.1.0</shiro-redis.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</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>
    
            <!-- druid数据库连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!--编写更少量的代码:使用apache commons工具类库:
            https://www.cnblogs.com/ITtangtang/p/3966955.html-->
            <!--apache.commons.lang3-->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
            </dependency>
    
            <!--你可以把这个工具看成是java.util的扩展-->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
                <version>${commons-collections4.version}</version>
            </dependency>
    
            <!--apache.codec:编码方法的工具类包
            https://blog.csdn.net/u012881904/article/details/52767853-->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>${shiro-spring.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.crazycake</groupId>
                <artifactId>shiro-redis</artifactId>
                <version>${shiro-redis.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    3、编辑application.yml

    我项目中使用了多环境配置,你们可根据自己情况修改

    mybatis-plus:
        configuration:
            map-underscore-to-camel-case: true
            use-generated-keys: true
        mapper-locations: classpath*:/mappers/**/*.xml
        type-aliases-package: com.leigq.www.shiro.domain.entity
    server:
        tomcat:
            uri-encoding: UTF-8
    spring:
        datasource:
            connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKPYsCl3alwZlRb1vKoFdVu0LP3Nm/+vH5iOWxI83pkUbrQc13Lxz/VT3D+H+ziaUpUsA+ZjG4iZGTDJWZnP8kcCAwEAAQ==
            driver-class-name: com.mysql.cj.jdbc.Driver
            filters: config,stat,wall,slf4j
            initialSize: 5
            maxActive: 20
            maxPoolPreparedStatementPerConnectionSize: 20
            maxWait: 60000
            minEvictableIdleTimeMillis: 300000
            minIdle: 5
            password: kGJF6c+pzVsf49LGs01ss0yijBGXIpNEp20cMkNCQo3ONaeMNPeoW9M89v+nGeiWs95/D2Ms59uGyydDGUWpmg==
            poolPreparedStatements: true
            testOnBorrow: false
            testOnReturn: false
            testWhileIdle: true
            timeBetweenEvictionRunsMillis: 60000
            type: com.alibaba.druid.pool.DruidDataSource
            url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
            username: root
            validationQuery: SELECT 1 FROM DUAL
        thymeleaf:
          cache: false
        redis:
          host: localhost
          port: 6379
          timeout: 2000s
          password: 111111
    

    4、创建MySessionManager

    package com.leigq.www.shiro.config;
    
    import org.apache.commons.lang3.StringUtils;
    import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.apache.shiro.web.util.WebUtils;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import java.io.Serializable;
    
    /**
     * 自定义session管理
     * <br/>
     * 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),
     * 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。
     * 自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
     * @author :leigq
     * @date :2019/7/1 10:52
     */
    public class MySessionManager extends DefaultWebSessionManager {
    
        private static final String AUTHORIZATION = "Authorization";
    
        private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    
        @Override
        protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
            String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
            //如果请求头中有 Authorization 则其值为sessionId
            if (!StringUtils.isEmpty(id)) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                return id;
            } else {
                //否则按默认规则从cookie取sessionId
                return super.getSessionId(request, response);
            }
        }
    }
    
    

    5、创建MyShiroRealm

    package com.leigq.www.shiro.config;
    
    import com.leigq.www.shiro.domain.entity.Permission;
    import com.leigq.www.shiro.domain.entity.Role;
    import com.leigq.www.shiro.domain.entity.User;
    import com.leigq.www.shiro.service.IPermissionService;
    import com.leigq.www.shiro.service.IRoleService;
    import com.leigq.www.shiro.service.IUserService;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    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;
    import java.util.List;
    import java.util.Objects;
    
    /**
     * @author :leigq
     * @date :2019/6/28 16:31
     * @description:自定义 shiroRealm, 主要是重写其认证、授权
     */
    @Slf4j
    public class MyShiroRealm extends AuthorizingRealm {
    
        @Resource
        private IUserService iUserService;
    
        @Resource
        private IRoleService iRoleService;
    
        @Resource
        private IPermissionService iPermissionService;
    
    
        /**
         * create by: leigq
         * description: 授权
         * create time: 2019/7/1 10:32
         *
         * @return 权限信息,包括角色以及权限
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            log.warn("开始执行授权操作.......");
    
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            //如果身份认证的时候没有传入User对象,这里只能取到userName
            //也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象
            User user = (User) principals.getPrimaryPrincipal();
    
            // 查询用户角色,一个用户可能有多个角色
            List<Role> roles = iRoleService.getUserRoles(user.getUserId());
    
            for (Role role : roles) {
                authorizationInfo.addRole(role.getRole());
                // 根据角色查询权限
                List<Permission> permissions = iPermissionService.getRolePermissions(role.getRoleId());
                for (Permission p : permissions) {
                    authorizationInfo.addStringPermission(p.getPermission());
                }
            }
            return authorizationInfo;
        }
    
        /**
         * create by: leigq
         * description: 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
         * create time: 2019/7/1 09:04
         *
         * @return 身份验证信息
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            log.warn("开始进行身份认证......");
    
            //获取用户的输入的账号.
            String userName = (String) token.getPrincipal();
    
            //通过username从数据库中查找 User对象.
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            User user = iUserService.findByUsername(userName);
            if (Objects.isNull(user)) {
                return null;
            }
    
            return new SimpleAuthenticationInfo(
                    // 这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限
                    user,
                    // 密码
                    user.getPassword(),
                    // salt = username + salt
                    ByteSource.Util.bytes(user.getCredentialsSalt()),
                    // realm name
                    getName()
            );
        }
    
    }
    
    

    6、创建ShiroConfig

    package com.leigq.www.shiro.config;
    
    import lombok.Data;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.session.mgt.SessionManager;
    import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.servlet.SimpleCookie;
    import org.crazycake.shiro.RedisCacheManager;
    import org.crazycake.shiro.RedisManager;
    import org.crazycake.shiro.RedisSessionDAO;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.time.Duration;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author :leigq
     * @date :2019/6/28 16:53
     * @description:shiro配置
     */
    @Configuration
    @ConfigurationProperties(
            prefix = "spring.redis"
    )
    @Data
    public class ShiroConfig {
    
        private String host = "localhost";
        private int port = 6379;
        private String password;
        private Duration timeout;
    
        /**
         * Filter工厂,设置对应的过滤条件和跳转条件
         * create by: leigq
         * create time: 2019/7/3 14:29
         *
         * @return ShiroFilterFactoryBean
         */
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
    
            // 过滤器链定义映射
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    
            /*
             * anon:所有url都都可以匿名访问,authc:所有url都必须认证通过才可以访问;
             * 过滤链定义,从上向下顺序执行,authc 应放在 anon 下面
             * */
            filterChainDefinitionMap.put("/login", "anon");
            // 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录
            filterChainDefinitionMap.put("/css/**", "anon");
            filterChainDefinitionMap.put("/fonts/**", "anon");
            filterChainDefinitionMap.put("/img/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/html/**", "anon");
            // 所有url都必须认证通过才可以访问
            filterChainDefinitionMap.put("/**", "authc");
    
            // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了, 位置放在 anon、authc下面
            filterChainDefinitionMap.put("/logout", "logout");
    
            // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
            // 配器shirot认登录累面地址,前后端分离中登录累面跳转应由前端路由控制,后台仅返回json数据, 对应LoginController中unauth请求
            shiroFilterFactoryBean.setLoginUrl("/un_auth");
    
            // 登录成功后要跳转的链接, 此项目是前后端分离,故此行注释掉,登录成功之后返回用户基本信息及token给前端
            // shiroFilterFactoryBean.setSuccessUrl("/index");
    
            // 未授权界面, 对应LoginController中 unauthorized 请求
            shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
    
        /**
         * 凭证匹配器(由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
         * create by: leigq
         * create time: 2019/7/3 14:30
         *
         * @return HashedCredentialsMatcher
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            // 散列算法:这里使用MD5算法;
            hashedCredentialsMatcher.setHashAlgorithmName("md5");
            // 散列的次数,比如散列两次,相当于 md5(md5(""));
            hashedCredentialsMatcher.setHashIterations(2);
            return hashedCredentialsMatcher;
        }
    
        /**
         * 将自己的验证方式加入容器
         * create by: leigq
         * create time: 2019/7/3 14:30
         *
         * @return MyShiroRealm
         */
        @Bean
        public MyShiroRealm myShiroRealm() {
            MyShiroRealm myShiroRealm = new MyShiroRealm();
            myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return myShiroRealm;
        }
    
        /**
         * RedisSessionDAO shiro sessionDao层的实现 通过redis, 使用的是shiro-redis开源插件
         * create by: leigq
         * create time: 2019/7/3 14:30
         *
         * @return RedisSessionDAO
         */
        @Bean
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
            redisSessionDAO.setExpire(1800);
            return redisSessionDAO;
        }
    
        /**
         * Session ID 生成器
         * <br/>
         * create by: leigq
         * <br/>
         * create time: 2019/7/3 16:08
         *
         * @return JavaUuidSessionIdGenerator
         */
        @Bean
        public JavaUuidSessionIdGenerator sessionIdGenerator() {
            return new JavaUuidSessionIdGenerator();
        }
    
        /**
         * 自定义sessionManager
         * create by: leigq
         * create time: 2019/7/3 14:31
         *
         * @return SessionManager
         */
        @Bean
        public SessionManager sessionManager() {
            MySessionManager mySessionManager = new MySessionManager();
            mySessionManager.setSessionDAO(redisSessionDAO());
            return mySessionManager;
        }
    
        /**
         * 配置shiro redisManager, 使用的是shiro-redis开源插件
         * <br/>
         * create by: leigq
         * <br/>
         * create time: 2019/7/3 14:33
         *
         * @return RedisManager
         */
        private RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host);
            redisManager.setPort(port);
            redisManager.setTimeout((int) timeout.toMillis());
            redisManager.setPassword(password);
            return redisManager;
        }
    
        /**
         * cacheManager 缓存 redis实现, 使用的是shiro-redis开源插件
         * <br/>
         * create by: leigq
         * <br/>
         * create time: 2019/7/3 14:33
         *
         * @return RedisCacheManager
         */
        @Bean
        public RedisCacheManager cacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            // 必须要设置主键名称,shiro-redis 插件用过这个缓存用户信息
            redisCacheManager.setPrincipalIdFieldName("userId");
            return redisCacheManager;
        }
    
        /**
         * create by: leigq
         * description: 权限管理,配置主要是Realm的管理认证
         * create time: 2019/7/1 10:09
         *
         * @return SecurityManager
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myShiroRealm());
            // 自定义session管理 使用redis
            securityManager.setSessionManager(sessionManager());
            // 自定义缓存实现 使用redis
            securityManager.setCacheManager(cacheManager());
            return securityManager;
        }
    
        /*
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        @Bean
        public SimpleCookie cookie() {
            // cookie的name,对应的默认是 JSESSIONID
            SimpleCookie cookie = new SimpleCookie("SHARE_JSESSIONID");
            cookie.setHttpOnly(true);
            //  path为 / 用于多个系统共享 JSESSIONID
            cookie.setPath("/");
            return cookie;
        }
    
        /* 此项目使用 shiro 场景为前后端分离项目,这里先注释掉,统一异常处理已在 GlobalExceptionHand.java 中实现 */
        /**
         * create by: leigq
         * description: 异常处理, 详见:https://www.cnblogs.com/libra0920/p/6289848.html
         * create time: 2019/7/1 10:28
         * @return SimpleMappingExceptionResolver
         */
    //    @Bean(name = "simpleMappingExceptionResolver")
    //    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
    //        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
    //        Properties mappings = new Properties();
    //        mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
    //        mappings.setProperty("UnauthorizedException", "/user/403");
    //        r.setExceptionMappings(mappings);  // None by default
    //        r.setDefaultErrorView("error");    // No default
    //        r.setExceptionAttribute("exception");     // Default is "exception"
    //        //r.setWarnLogCategory("example.MvcLogger");     // No default
    //        return r;
    //    }
    }
    

    7、创建LoginController

    package com.leigq.www.shiro.controller;
    
    import com.leigq.www.shiro.bean.CacheUser;
    import com.leigq.www.shiro.bean.Response;
    import com.leigq.www.shiro.domain.entity.User;
    import com.leigq.www.shiro.service.IUserService;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * @author :leigq
     * @date :2019/6/28 16:55
     * @description:登录Controller
     */
    @Slf4j
    @RestController
    public class LoginController {
    
        @Resource
        private IUserService iUserService;
    
        @Resource
        private Response response;
    
        /**
         * create by: leigq
         * description: 登录
         * create time: 2019/6/28 17:11
         *
         * @return 登录结果
         */
        @PostMapping("/login")
        public Response login(User user) {
            log.warn("进入登录.....");
    
            String userName = user.getUserName();
            String password = user.getPassword();
    
            if (StringUtils.isBlank(userName)) {
                return response.failure("用户名为空!");
            }
    
            if (StringUtils.isBlank(password)) {
                return response.failure("密码为空!");
            }
    
            CacheUser loginUser = iUserService.login(userName, password);
            // 登录成功返回用户信息
            return response.success("登录成功!", loginUser);
        }
    
        /**
         * create by: leigq
         * description: 登出
         * create time: 2019/6/28 17:37
         */
        @GetMapping("/logout")
        public Response logOut() {
            iUserService.logout();
            return response.success("登出成功!");
        }
    
        /**
         * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
         * <br/>
         * create by: leigq
         * <br/>
         * create time: 2019/7/3 14:53
         * @return  
         */
        @RequestMapping("/un_auth")
        public Response unAuth() {
            return response.failure(HttpStatus.UNAUTHORIZED, "用户未登录!", null);
        }
    
        /**
         * 未授权,无权限,此处返回未授权状态信息由前端控制跳转页面
         * <br/>
         * create by: leigq
         * <br/>
         * create time: 2019/7/3 14:53
         * @return
         */
        @RequestMapping("/unauthorized")
        public Response unauthorized() {
            return response.failure(HttpStatus.FORBIDDEN, "用户无权限!", null);
        }
    }
    
    

    8、具体登录方法

    package com.leigq.www.shiro.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.leigq.www.shiro.bean.CacheUser;
    import com.leigq.www.shiro.domain.entity.User;
    import com.leigq.www.shiro.domain.mapper.UserMapper;
    import com.leigq.www.shiro.service.IUserService;
    import com.leigq.www.shiro.web.exception.LoginException;
    import lombok.extern.slf4j.Slf4j;
    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.subject.Subject;
    import org.springframework.beans.BeanUtils;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author leigq
     * @since 2019-06-28
     */
    @Service
    @Slf4j
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public User findByUsername(String username) {
            return baseMapper.selectOne(
                    new LambdaQueryWrapper<User>().eq(User::getUserName, username)
            );
        }
    
        @Override
        public CacheUser login(String userName, String password) {
    
            // 获取Subject实例对象,用户实例
            Subject currentUser = SecurityUtils.getSubject();
    
            // 将用户名和密码封装到UsernamePasswordToken
            UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
    
            CacheUser cacheUser;
    
            // 4、认证
            try {
                // 传到 MyShiroRealm 类中的方法进行认证
                currentUser.login(token);
                // 构建缓存用户信息返回给前端
                User user = (User) currentUser.getPrincipals().getPrimaryPrincipal();
                cacheUser = CacheUser.builder()
                        .token(currentUser.getSession().getId().toString())
                        .build();
                BeanUtils.copyProperties(user, cacheUser);
                log.warn("CacheUser is {}", cacheUser.toString());
            } catch (UnknownAccountException e) {
                log.error("账户不存在异常:", e);
                throw new LoginException("账号不存在!", e);
            } catch (IncorrectCredentialsException e) {
                log.error("凭据错误(密码错误)异常:", e);
                throw new LoginException("密码不正确!", e);
            } catch (AuthenticationException e) {
                log.error("身份验证异常:", e);
                throw new LoginException("用户验证失败!", e);
            }
            return cacheUser;
        }
    
        @Override
        public void logout() {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
        }
    
        @Override
        public List<User> listUsers() {
            return baseMapper.selectList(new LambdaQueryWrapper<>());
        }
    }
    

    上面我列出了项目中主要的几个类,大家可以对照着项目看,每个类中的注释已经写的很详细了。

    使用及测试

    我们配置每个接口的权限使用@RequiresPermissions("user:view")注解即可,其中user:view对应权限表中的权限。
    在这里插入图片描述
    1、登录测试

    在这里插入图片描述
    在这里插入图片描述
    登录成功会将用户信息存入缓存。

    2、请求查询用户接口

    我们先输入错误的token试试
    在这里插入图片描述
    我们再输入正确的token试试
    在这里插入图片描述
    3、请求用户删除接口

    在这里插入图片描述
    因为我们没有给此用户配置此权限,所以返回无权限

    4、退出登录

    在这里插入图片描述
    我们再请求用户列表接口

    在这里插入图片描述

    感谢


    作者:不敲代码的攻城狮
    出处:https://www.cnblogs.com/leigq/
    任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人能读懂的代码。

     
  • 相关阅读:
    Mono项目将继续推动基于Linux的开发
    VS.PHP 在Visual Studio 下的 PHP 开发 IDE 工具
    SQL Server 2008 的代码提示功能
    想做的时候没有机会了
    我的最爱
    双缓冲
    做个好男人!
    再见了,曾经喜欢过的歌手
    看看他是喜欢你还是爱你的~~~
    独家:未来五年程序员应当具备的十项技能
  • 原文地址:https://www.cnblogs.com/leigq/p/13406543.html
Copyright © 2011-2022 走看看