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、退出登錄

在這裏插入圖片描述
我們再請求用戶列表接口

在這裏插入圖片描述

感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章