Rampant

Spring Security 微服务权限管理

Spring Security:作为一个基于Spring的优秀Web网站安全框架,除了能在单体的微服务中对权限进行拦截,我们还可以应用到微服务架构中。实现单点登录权限授权等功能。

在Spring Security中实现微服务的权限管理,最好结合 JWT,那么接下来我们先了解一下什么是JWT

JWT(Json Web Token)

概述

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

起源

在Web安全认证中经常用到的就是传统的基于Session的安全验证,和新崛起的JWT

传统的Session认证:

我们都知道Http请求是一种无状态的请求协议,当用户发送请求时我们,并不知道是谁发送的请求,那么在传统的方案中,用户在第一次请求时就会要求用户就行授权登录,授权成功服务器就会将请求的用户信息保存在Session中,并且给用户响应一个加密的Cookie,下次访问时带上Cookie,服务器通过解析Cookie,来判断用户是否登录。但是基于Session的保存方式随之互联网的发展,会渐渐的出现很多问题。

Session认证 缺点

  1. Session的保存位置是在内存中,当网站的用户量增大时,服务器的大量内存都用来保存Session,那么提供资源的服务也会降低,速度减慢。
  2. 在如今的微服务架构中,Session是保存在一台服务器的内存中,然而互联网的发展,微服务集群,服务拆分都是常事,那么对用户在访问时不同服务器都需要授权。扩展能力差。
  3. CSRF存放在Cookies中,那么当Cookies被盗取时,很可能会受到黑客的伪装请求攻击,导致服务器宕机。

基于JWT的授权认证

基于JWT的授权认证,他不需要服务器存放用户信息,而是将用户请求的信息保存到数据库或者(NoSQL数据库),推荐一款超级快的Redis的NoSQL数据库。具体实现原理如下。

  1. 用户在第一次请求的时候,需要经行安全授权验证。
  2. 验证成功后服务器会将用户授权的信息利用JWT加密生成Token
  3. Token响应给用户,并且将Token保存到Redis中(可以是数据库,Redis基于内存更快)。
  4. 用户将接收到的Token保存起来(前端工程师完成)。
  5. 第二次请求时,只需要在请求头中携带Token,访问即可。
  6. 浏览器在接收到请求后,利用Redis,返回保存的Token,并经行校验。校验成功就放行
  7. 利用Redis,可以解决多台服务器之间的授权问题,实现单点登录(单点登录就是解决Session缺点的第二点的)

JWT组成

JWT的实际形式使用三部分组成,并通过 . 来分割

# 主要组成的形式
xxxxx.yyyyy.zzzzz

这三部分分别是HeaderPayloadSignature

  • Header(头),typ:属于什么类型的加密、alg:加密方法 。通过对上面的定义在通过Base64Url编码形成第一部分。
{
    \'typ\': \'JWT\'
    \'alg\': \'HS256\'
}
  • Payload(载荷),携带的参数,主要分为

    • sub: 标准的声明
    • name:公共的声明
    • admin:私有的声明

    通过对上面的定义在通过Base64Url编码形成第二部分。

{
    \'sub\': \'123456\'
    \'name\': \'john\'
    \'admin\': true
}
  • Signature,是对上面两个加密的结果在经行加盐加密
    • 简单来说就是利用上面两个部分的加密结果,在经行加盐加密,形成第三部分
var encodestring = base64UrlEncode(Header)+`.`+base64UrlEncode(payload)
var signature = HMACSHA256(encodestring, \'salt\')

JWT的使用

  1. 构建一个Maven项目

  2. 导入以下依赖

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    <!--JDK 1.8 以上需要加入以下依赖-->
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-core</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>
    
  3. 测试代码

    import io.jsonwebtoken.*;
    
    import java.util.Date;
    import java.util.UUID;
    
    public class JWT {
        public static void main(String[] args) {
    
            String salt = "salt";   // 加密的盐
    
            JwtBuilder jwtBuilder = Jwts.builder();
            String jwtToken = jwtBuilder
                    // header 设置
                    .setHeaderParam("typ","JWT")
                    .setHeaderParam("alg","HS256")
                    // Payload 设置
                    .claim("username","tom")
                    .claim("role","admin")
                    .setSubject("admin-test")
                    // 过期时间
                    .setExpiration(new Date(System.currentTimeMillis()+(1000*60*60*24)))
                    // 设置 id
                    .setId(UUID.randomUUID().toString())
                    // Signature 设置,主要是加密方式,和加盐参数
                    .signWith(SignatureAlgorithm.HS256,salt)
                    // 拼接
                    .compact();
            System.out.println(jwtToken);
            // 调用解析方法
            JWT.jwtDecrypt(jwtToken,salt);
        }
    
        public static void jwtDecrypt(String encryptionPassword,String salt){
            // 传入加盐的参数,和加密对象
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(salt).parseClaimsJws(encryptionPassword);
            // 解析Body(Payload)部分的值
            Claims body = claimsJws.getBody();
            System.out.println(body.get("username"));
            System.out.println(body.get("role"));
            System.out.println(body.getId());
            System.out.println(body.getExpiration());
            // 解析头部的值
            Header header = claimsJws.getHeader();
            System.out.println(header.get("typ"));
            System.out.println(header.get("alg"));
        }
    }
    

基于Spring Security和JWT的微服务权限校验

数据库准备

  1. 在数据库终准备三个表,分别是用户表角色表用户角色关系表

  2. 建表语句如下

    CREATE TABLE `jwt_role` (
        `id` char(19) NOT NULL DEFAULT \'\' COMMENT \'角色id\',
        `role_name` varchar(20) NOT NULL DEFAULT \'\' COMMENT \'角色名称\',
        `role_code` varchar(20) DEFAULT NULL COMMENT \'角色编码\',
        `remark` varchar(255) DEFAULT NULL COMMENT \'备注\',
        `deleted` tinyint(1) unsigned NOT NULL DEFAULT \'0\' COMMENT \'逻辑删除 1(true)已删除, 0(false)未删除\',
        `version` int(1) NOT NULL DEFAULT 1 COMMENT \'版本控制,乐观锁\',
        `create_time` datetime NOT NULL COMMENT \'创建时间\',
        `update_time` datetime NOT NULL COMMENT \'更新时间\',
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    CREATE TABLE `jwt_user` (
        `id` char(19) NOT NULL COMMENT \'用户id\',
        `username` varchar(20) NOT NULL DEFAULT \'\' COMMENT \'用户名\',
        `password` varchar(32) NOT NULL DEFAULT \'\' COMMENT \'用户密码\',
        `salt` varchar(100) NOT NULL DEFAULT \'\' COMMENT \'密码加密盐\',
        `nick_name` varchar(50) DEFAULT NULL COMMENT \'昵称\',
        `head_portrait` varchar(255) DEFAULT NULL COMMENT \'用户头像\',
        `deleted` tinyint(1) unsigned NOT NULL DEFAULT \'0\' COMMENT \'逻辑删除 1(true)已删除, 0(false)未删除\',
        `version` int(1) NOT NULL DEFAULT 1 COMMENT \'版本控制,乐观锁\',
        `create_time` datetime NOT NULL COMMENT \'创建时间\',
        `update_time` datetime NOT NULL COMMENT \'更新时间\',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_username` (`username`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\'用户表\';
    
    CREATE TABLE `jwt_user_role` (
        `id` char(19) NOT NULL DEFAULT \'\' COMMENT \'主键id\',
        `role_id` char(19) NOT NULL DEFAULT \'0\' COMMENT \'角色id\',
        `user_id` char(19) NOT NULL DEFAULT \'0\' COMMENT \'用户id\',
        `deleted` tinyint(1) unsigned NOT NULL DEFAULT \'0\' COMMENT \'逻辑删除 1(true)已删除, 0(false)未删除\',
        `version` int(1) NOT NULL DEFAULT 1 COMMENT \'版本控制,乐观锁\',
        `create_time` datetime NOT NULL COMMENT \'创建时间\',
        `update_time` datetime NOT NULL COMMENT \'更新时间\',
        PRIMARY KEY (`id`),
        KEY `idx_role_id` (`role_id`),
        KEY `idx_user_id` (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    insert into jwt_user(id, username, password, salt, nick_name, head_portrait, deleted, version, create_time, update_time) VALUES (1,\'admin\',\'adbc396f4cd1b6b1f41967bd8a857dcc\',\'admin\',\'橘子有点甜\',null,0,1,now(),now()), (2,\'admin1\',\'adbc396f4cd1b6b1f41967bd8a857dcc\',\'admin\',\'橘子有点甜2\',null,0,1,now(),now())
    
    insert into jwt_role (id, role_name, role_code, remark, deleted, version, create_time, update_time) VALUES (1,\'管理员\',\'admin\',\'可以管理所有模块\',0,1,now(),now()),(2,\'测试员\',\'test\',\'可以管理测试模块\',0,1,now(),now())
    
      insert into jwt_user_role (id, role_id, user_id, deleted, version, create_time, update_time) VALUES (1,1,1,0,1,now(),now()),(2,2,1,0,1,now(),now()),(3,2,2,0,1,now(),now())
    

注册中心准备(Nacos)

Nacos 如何安装和启动,参考如下教程:Nacos 安装启动教程

NoSQL数据库准备(Redis)

Redis如何安装启动,参考如下教程:Redis安装教程

安装完成,(如果是远程服务器)默认我们的项目是连接不了的,需要修改一些配置,参考如下地址修改配置后重新启动

https://www.cnblogs.com/swda/p/12013439.html

项目搭建

创建父工程

  1. 创建一个空的Maven项目

  2. 删除src目录

  3. 导入如下依赖

    <?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>
    
        <groupId>com.wyx</groupId>
        <artifactId>SpringSecurity-JWT</artifactId>
        <packaging>pom</packaging>
        <version>1.0</version>
    
        <properties>
            <project.build.sourceEmcoding>UTF-8</project.build.sourceEmcoding>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <!--对应的版本-->
            <spring.boot.dependencies.version>2.4.3</spring.boot.dependencies.version>
            <spring.cloud.dependencies.version>2020.0.2</spring.cloud.dependencies.version>
            <spring.cloud.alibaba.dependencies.version>2.2.1.RELEASE</spring.cloud.alibaba.dependencies.version>
            <mysql.version>8.0.23</mysql.version>
            <log4j.version>1.2.17</log4j.version>
            <junit.version>4.13</junit.version>
            <lombok.version>1.18.20</lombok.version>
            <mybatis.plus.boot.starter.version>3.4.2</mybatis.plus.boot.starter.version>
            <mybatis.plus.generator.version>3.4.1</mybatis.plus.generator.version>
            <velocity.version>2.2</velocity.version>
            <jwt.version>0.9.1</jwt.version>
            <swagger.version>2.9.2</swagger.version>
            <hutool.version>5.7.2</hutool.version>
    
        </properties>
    
        <dependencyManagement>
            <dependencies>
                <!--SpringBoot 依赖-->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>${spring.boot.dependencies.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <!--Spring Cloud 依赖-->
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring.cloud.dependencies.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <!--Spring Cloud Alibaba 依赖-->
                <dependency>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                    <version>${spring.cloud.alibaba.dependencies.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <!--Mysql 连接驱动-->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>${mysql.version}</version>
                </dependency>
                <!--Lombok 依赖-->
                <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>${lombok.version}</version>
                </dependency>
                <!--log4j 依赖-->
                <dependency>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                    <version>${log4j.version}</version>
                </dependency>
                <!--junit 单元测试 依赖-->
                <dependency>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                    <version>${junit.version}</version>
                </dependency>
                <!--mybatis-plus整合SpringBoot依赖-->
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                    <version>${mybatis.plus.boot.starter.version}</version>
                </dependency>
                <!--代码生成器依赖-->
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-generator</artifactId>
                    <version>${mybatis.plus.generator.version}</version>
                </dependency>
                <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
                <dependency>
                    <groupId>org.apache.velocity</groupId>
                    <artifactId>velocity-engine-core</artifactId>
                    <version>${velocity.version}</version>
                </dependency>
    
                <!-- JWT -->
                <dependency>
                    <groupId>io.jsonwebtoken</groupId>
                    <artifactId>jjwt</artifactId>
                    <version>${jwt.version}</version>
                </dependency>
    
                <!--swagger-->
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger2</artifactId>
                    <version>${swagger.version}</version>
                </dependency>
    
                <!--swagger ui-->
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger-ui</artifactId>
                    <version>${swagger.version}</version>
                </dependency>
                <!--HuTool工具包-->
                <dependency>
                    <groupId>cn.hutool</groupId>
                    <artifactId>hutool-all</artifactId>
                    <version>${hutool.version}</version>
                </dependency>
                
            </dependencies>
        </dependencyManagement>
        
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.4.4</version>
                    <configuration>
                        <fork>true</fork>
                        <addResources>true</addResources>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

创建工具包API接口

  1. 创建新模块 common-api

  2. 导入pom.xml

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>
    
  3. 编写工具类

    统一返回类型

    package com.wyx.utils;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.HashMap;
    
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class CommonResult<T> {
    
    
        private Integer code;
        private String message;
        private T data;
    
    
        public static CommonResult Success(Object data){
            return new CommonResult(200,"success",data);
        }
    
        public static CommonResult Error(){
            HashMap<String, String> map = new HashMap<>();
            map.put("warning",String.valueOf("请求出错,请联系管理员"));
            return new CommonResult(444,"error",map);
        }
    
        public static CommonResult isNull(){
            HashMap<String, String> map = new HashMap<>();
            map.put("warning",String.valueOf("请求数据为空,请联系管理员"));
            return new CommonResult(404,"error",map);
        }
    
        public static CommonResult perNoAll(){
            HashMap<String, String> map = new HashMap<>();
            map.put("message",String.valueOf("权限不允许,请联系管理员"));
            return new CommonResult(401,"warning",map);
        }
    
        public static CommonResult ErrorMethod(){
            HashMap<String, String> map = new HashMap<>();
            map.put("message",String.valueOf("请求方法错误,请查看请求方法"));
            return new CommonResult(403,"warning",map);
        }
    
        public static CommonResult Exception(){
            HashMap<String, String> map = new HashMap<>();
            map.put("message",String.valueOf("服务器内部错误,请联系管理员"));
            return new CommonResult(500,"error",map);
        }
    
        public static CommonResult MyMessage (String message){
            HashMap<String, String> map = new HashMap<>();
            map.put("message",message);
            return new CommonResult(911,"warning",map);
        }
    
    }
    

    密码加密工具类

    package com.wyx.utils;
    
    import cn.hutool.crypto.digest.DigestUtil;
    
    /**
     * 对传入的密码经行加密
     * @ClassName UserPasswordEncryption
     * @Description TODO
     * @Author 王玉星
     * @Date 2021/8/3 13:57
     * @Version 1.0
     */
    public class PasswordEncryption {
    
        /**
         * 使用 MD5 方式对用户传入的密码经行加密
         * @param password 用户传入的密码
         * @param salt  加密的盐,最少要 4 位数
         *
         * @return  使用用户密码加上加密盐,加密后的MD5值
         */
        public static String encryption(String password, String salt){
            String saltPassword = salt.charAt(2)+salt.charAt(3)+password+salt.charAt(0)+salt.charAt(1);
            return DigestUtil.md5Hex(saltPassword);
        }
    
    }
    

    JWT加密解密类

    package com.wyx.utils;
    
    import io.jsonwebtoken.*;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    
    /**
     *  对用户信息利用JWT的方式设置加密解密功能
     * @ClassName LoginToken
     * @Description None
     * @Author 王玉星
     * @Date 2021/8/1 14:12
     * @Version 1.0
     */
    public class TokenManager {
    
        //推荐用公司名
        private static String SALT = "con.wyx";
        // 过期时间 7 天
        private static Integer EXPIRE_TIME  = 1000*60*60*24*7;
    
    
        public static String encryption(Long id, String username){
            return encryption(id,username,"");
        }
        /**
         * 加密用户信息函数
         * @param id 用户Id
         * @param username 用户姓名
         * @param role  用户角色,输入时用,隔开。代表多个角色,形如 "admin,user"
         *
         * @return 经过JWT加密后的密文
         */
        public static String encryption(Long id, String username, String role){
    
            String[] roles = role.split(",");
            JwtBuilder jwtBuilder = Jwts.builder();
            String jwtPassword = jwtBuilder
                    // header 设置
                    .setHeaderParam("typ","JWT")
                    .setHeaderParam("alg","HS256")
                    // Payload 设置
                    .claim("username",username)
                    .claim("role",roles)
                    .setExpiration(new Date(System.currentTimeMillis()+EXPIRE_TIME))
                    // 设置 id
                    .setId(String.valueOf(id))
                    // Signature 设置,主要是加密方式,和加盐参数
                    .signWith(SignatureAlgorithm.HS256,SALT)
                    .compact();
            return jwtPassword;
        }
    
        /**
         * 解析一个加密的密文,并将他封装成 Map 对象返回,
         * username:用户名,
         * roles:角色,
         * id:用户ID,
         * expireTime:过期时间,
         * head_typ:加密类型,
         * head_alg:加密方式,
         * @param jwtPassword 加密的密文
         *
         * @return  封装了用户加密信息的 map 对象
         */
        public static HashMap jwtDecrypt(String jwtPassword){
            HashMap<String, Object> decryMap = new HashMap<>();
            // 传入加盐的参数,和加密对象,这里切记不能定义变量,一行写完
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SALT).parseClaimsJws(jwtPassword);
            // 解析Body(Payload)部分的值
            Claims body = claimsJws.getBody();
            decryMap.put("username", body.get("username"));
            decryMap.put("roles", body.get("role"));
            decryMap.put("id", body.getId());
            decryMap.put("expireTime", body.getExpiration());
            // 解析头部的值
            Header header = claimsJws.getHeader();
            decryMap.put("head_typ",header.get("typ"));
            decryMap.put("head_alg",header.get("alg"));
            return decryMap;
        }
    }
    

    如果自己还有工具包,可以往这个项目中放,后期好调用,这里只是给出几个简单常用的工具包。

创建配置类接口

该项目主要是用于对Spring容器的一些配置,比如Redis的操作模板,Swagger配置,Mybatis-Plus的配置,等等,许多配置都可以放到这里面来,方便后期管理,主要是配置进入Spring容器的

  1. 创建项目Configuration-API

  2. 导入依赖,依赖根据自己在项目配置中需要什么依赖,就导入什么依赖,当前自需要配置以下三个,如果有更多配置,修改依赖即可

    <dependencies>
        <dependency>
            <groupId>com.wyx</groupId>
            <artifactId>common-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--对Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <!--Mybatis-Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
    
        <!--引入swagger bootstrap ui-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
        </dependency>
    </dependencies>
    
    <!--资源过滤器-->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  3. Mybatis-Plus自动填充注解

    package com.wyx.MybatisPlusConfig;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    /**
     *  Mybatis-Plus 中的字段填充配置类
     */
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
    
        @Override
        public void insertFill(MetaObject metaObject) {
            //  执行插入操作时,查找对应的字段名,和给它更新的值,并将元数据带入
            this.setFieldValByName("createTime",new Date(),metaObject);
            this.setFieldValByName("updateTime",new Date(),metaObject);
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            this.setFieldValByName("updateTime",new Date(),metaObject);
        }
    }
    

    Mybatis-Plus乐观锁,分页插件配置类

    package com.wyx.MybatisPlusConfig;
    
    
    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @MapperScan("com.wyx.mapper,com.wyx.SpringSecurityConfig.mapper")
    //@MapperScan("com.wyx.mapper") 开始时是配置在主启动类上,当我们创建Mybatis-Plus的配置类时,一般情况下把它移动到我们的配置类上
    public class MybatisPlusConfig {
        /**
         *
         旧版
        @Bean
        public OptimisticLockerInterceptor optimisticLockerInterceptor() {
            return new OptimisticLockerInterceptor();
        }
        // 旧版
         @Bean
         public PaginationInterceptor paginationInterceptor() {
         PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
         // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         // paginationInterceptor.setOverflow(false);
         // 设置最大单页限制数量,默认 500 条,-1 不受限制
         // paginationInterceptor.setLimit(500);
         // 开启 count 的 join 优化,只针对部分 left join
         paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
         return paginationInterceptor;
         }
         */
        
        /**
         * 乐观锁插件,分页插件
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
            // 乐观锁
            mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            // 分页插件
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
            return mybatisPlusInterceptor;
        }
    
    }
    
  4. Swagger配置类

    package com.wyx.SwaggerConfig;
    
    import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
    import com.google.common.base.Predicates;
    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
    @EnableSwaggerBootstrapUI
    public class Swagger2 {
    
        //默认文档地址(swagger-ui)为 http://localhost:端口号/swagger-ui.html
        //默认文档地址(bootstrap-ui)为 http://localhost:端口号/doc.html
        @Bean
        public Docket createRestApi(){
    
            return new Docket(DocumentationType.SWAGGER_2)          //指定Api类型为Swagger2
                    .apiInfo(apiInfo())                             //指定文档汇总信息
                    .select()
                    .apis(RequestHandlerSelectors
                            .basePackage("com.wyx.controller")) //指定controller包路径
                    //.paths(PathSelectors.any())                     //指定展示所有controller
                    .paths(Predicates.not(PathSelectors.regex("/error.*")))     // 排除哪些路径不扫描
                    .build();
        }
        private ApiInfo apiInfo(){
            //返回一个apiinfo
            return new ApiInfoBuilder()
                    .title("api接口文档")                                       //文档页标题
                    .contact(
                            new Contact(
                                    "王玉星",
                                    "https://www.cnblogs.com/Rampant/",
                                    "309597117@qq.com")
                    )                                                           // 联系人信息
                    .description("api文档")                                       // 详细信息
                    .version("1.0.1")                                           // 文档版本号
                    .termsOfServiceUrl("https://www.cnblogs.com/Rampant/")                  //网站地址
                    .build();
        }
    
    }
    
  5. Redis模板和缓存配置

    package com.wyx.RedisConfig;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    import java.time.Duration;
    
    @EnableCaching //开启缓存
    @Configuration  //配置类
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            template.setConnectionFactory(factory);
            //key序列化方式
            template.setKeySerializer(redisSerializer);
            //value序列化
            template.setValueSerializer(jackson2JsonRedisSerializer);
            //value hashmap序列化
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            return template;
        }
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory factory) {
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            //解决查询缓存转换异常的问题
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            // 配置序列化(解决乱码的问题),过期时间600秒
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(600))
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                    .disableCachingNullValues();
            RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                    .cacheDefaults(config)
                    .build();
            return cacheManager;
    
        }
    }
    
  6. 异常处理类

    package com.wyx.exceptionhandler;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor  //生成有参数构造方法
    @NoArgsConstructor   //生成无参数构造
    public class MyException extends RuntimeException {
    
        private Integer code;//状态码
        private String msg;//异常信息
    
    }
    
    package com.wyx.exceptionhandler;
    
    import com.wyx.utils.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @ControllerAdvice
    @Slf4j
    public class GlobalExceptionHandler {
    
        //指定出现什么异常执行这个方法
        @ExceptionHandler(Exception.class)
        @ResponseBody //为了返回数据
        public CommonResult error(Exception e) {
            log.error("处理异常");
            e.printStackTrace();
            return CommonResult.Error();
        }
    
        //特定异常
        @ExceptionHandler(ArithmeticException.class)
        @ResponseBody //为了返回数据
        public CommonResult error(ArithmeticException e) {
            log.error("算术处理异常");
            e.printStackTrace();
            return CommonResult.Error();
        }
    
        //自定义异常
        @ExceptionHandler(MyException.class)
        @ResponseBody //为了返回数据
        public CommonResult error(MyException e) {
            log.error(e.getMessage());
            e.printStackTrace();
            return CommonResult.Error();
        }
    }
    
  7. SpringBoot自定义错误返回

    package com.wyx.springbootconfig;
    
    import com.wyx.utils.CommonResult;
    import org.springframework.boot.web.servlet.error.ErrorController;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * @ClassName MyErrorController
     * @Description TODO
     * @Author 王玉星
     * @Date 2021/8/8 23:42
     * @Version 1.0
     */
    
    @RestController
    public class MyErrorController implements ErrorController {
    
        @RequestMapping("/error")
        public CommonResult handleError(HttpServletRequest request){
            //获取statusCode:401,404,500
            Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
            if(statusCode == 401){
                return CommonResult.perNoAll();
            }else if(statusCode == 404){
                return CommonResult.isNull();
            }else if(statusCode == 403){
                return CommonResult.ErrorMethod();
            }else{
                return CommonResult.MyMessage("出现了未知异常,请联系管理员");
            }
    
        }
        @Override
        public String getErrorPath() {
            return "/error";
        }
    }
    

SpringSecurity安全配置(重点)

  1. 自定义SpringSecurity加密配置,如果时用户注册时请调用 encodeRandom方法,随机生成加密密文,和加密盐

    package com.wyx.SpringSecurityConfig;
    
    
    import com.wyx.utils.PasswordEncryption;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Random;
    
    /**
     *  自定义密码加密功能
     *
     */
    @Component
    public class MyPasswordEncoder implements PasswordEncoder {
    
        private String salt;
    
        @Autowired
        HttpServletRequest request;
    
        public MyPasswordEncoder() {
            this(-1);
        }
    
        public MyPasswordEncoder(int strength) {
        }
    
    
        /**
         * 用于用户注册时使用
         * 对密码使用随机的密码经行加密,返回加密的值,和加密盐,返回形式如下:“加密值,加密的随机盐”
         * @param rawPassword 传入密码
         *
         * @return  返回加密的密码和随机生成的盐,两者通过,分开。
         */
        public String encodeRandom(CharSequence rawPassword) {
            String randomSalt = setRandomSalt();
            return PasswordEncryption.encryption((String) rawPassword,randomSalt)+","+randomSalt;
        }
    
    
        /**
         * 对密码经行加密的函数
         * @param rawPassword 输入的密码
         *
         * @return  加密后返回的密码
         */
        @Override
        public String encode(CharSequence rawPassword) {
            setSalt();
            return PasswordEncryption.encryption((String) rawPassword,getSalt());
        }
    
        /**
         * 对输入的密码与数据库密码经行对比
         * @param rawPassword 输入的密码
         * @param encodedPassword   数据库中查出来的密码
         *
         * @return  返回对比结果true false
         */
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals((String.valueOf(rawPassword)));
        }
    
    
        /**
         * 放回随机加密的盐
         * @return String 随机加密的盐
         */
        public String setRandomSalt(){
    
            String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            Random random=new Random();
            StringBuffer sb=new StringBuffer();
            for(int i=0;i<6;i++){
                int number=random.nextInt(62);
                sb.append(str.charAt(number));
            }
            return sb.toString();
        }
    
    
        public String getSalt() {
            return salt;
        }
    
        public void setSalt() {
            this.salt = (String) request.getAttribute("loginSalt");
        }
    }
    
    
  2. 权限不通过的处理类

    package com.wyx.SpringSecurityConfig;
    
    import lombok.extern.java.Log;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Log
    public class UnauthEntryPoint implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            httpServletRequest.getRequestDispatcher("/authority/noAuthority").forward(httpServletRequest,httpServletResponse);
            httpServletRequest.getServletPath();
            log.warning("请求路径:"+httpServletRequest.getServletPath()+",权限不允许");
        }
    }
    
  3. 登录授权用户实体类

    package com.wyx.SpringSecurityConfig.pojo;
    
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    import lombok.NoArgsConstructor;
    
    import java.io.Serializable;
    
    /**
     * @ClassName SecurityUserMapper
     * @Description TODO
     * @Author 王玉星
     * @Date 2021/8/6 17:33
     * @Version 1.0
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @EqualsAndHashCode(callSuper = false)
    @TableName("jwt_user")
    @ApiModel(value="User对象", description="用户表")
    public class SecurityUser implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @ApiModelProperty(value = "用户id")
        @TableId(value = "id", type = IdType.ASSIGN_ID)
        private String id;
    
        @ApiModelProperty(value = "用户名")
        private String username;
    
        @ApiModelProperty(value = "用户密码")
        private String password;
    
        @ApiModelProperty(value = "密码加密盐")
        private String salt;
    
    }
    
  4. 登录查询接口

    package com.wyx.SpringSecurityConfig.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.wyx.SpringSecurityConfig.pojo.SecurityUser;
    
    import java.util.List;
    
    /**
     * <p>
     * 登录用户 Mapper 接口
     * </p>
     *
     * @author 王玉星
     * @since 2021-08-03
     */
    public interface SecurityUserMapper extends BaseMapper<SecurityUserMapper> {
    
        SecurityUser getSecurityUserByUserName(String username);
    
        List<String> getRoleListByUserName(String username);
    
    }
    
  5. 自定义登录查询Mapper.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper">
    
        <resultMap id="securityUser" type="com.wyx.SpringSecurityConfig.pojo.SecurityUser">
            <result column="id" property="id"/>
            <result column="password" property="password"/>
            <result column="salt" property="salt"/>
            <result column="username" property="username"/>
        </resultMap>
    
        <select id="getSecurityUserByUserName" resultMap="securityUser" parameterType="string">
            select id,username,salt,password
            from jwt_user
            where username = #{username}
        </select>
    
        <select id="getRoleListByUserName" resultType="java.lang.String">
            select role_code
            from jwt_role
            where id in (select role_id
                         from jwt_user_role
                         where user_id in (select id
                                           from jwt_user
                                           where username = #{username})
            )
        </select>
    
    </mapper>
    
  6. 授权处理器

    package com.wyx.SpringSecurityConfig;
    
    import com.wyx.exceptionhandler.MyException;
    import com.wyx.utils.CommonResult;
    import com.wyx.utils.TokenManager;
    import lombok.extern.java.Log;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    
    public class TokenAuthFilter extends BasicAuthenticationFilter {
    
    
        private RedisTemplate redisTemplate;
    
        private AuthenticationManager authenticationManager;
    
        public TokenAuthFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
            super(authenticationManager);
            this.redisTemplate = redisTemplate;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            //获取当前认证成功用户权限信息
            UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
            //判断如果有权限信息,放到权限上下文中
            if(authRequest != null) {
                SecurityContextHolder.getContext().setAuthentication(authRequest);
            }
            chain.doFilter(request,response);
        }
    
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
            //从header获取token
            String token = request.getHeader("token");
            if(token != null) {
                HashMap tokenMap = null;
                try {
                    tokenMap = TokenManager.jwtDecrypt(token);
                }catch (RuntimeException e){
                    logger.error("令牌已过期");
                    return null;
                }
                String username = (String) tokenMap.get("username");
                if (!redisTemplate.opsForValue().get(username).equals(token)){
                    System.out.println(token);
                    System.out.println(redisTemplate.opsForValue().get(username));
                    logger.error("令牌错误");
                    return null;
                }
                ArrayList roles = (ArrayList) tokenMap.get("roles");
                StringBuilder authRoles = new StringBuilder();
                for (Object role : roles) {
                    role = "ROLE_"+role.toString();
                    authRoles.append(role);
                    authRoles.append(",");
                }
                // 消除最后一个分号
                authRoles.replace(authRoles.length()-1,authRoles.length(),"");
    
    /*
                //从redis获取对应权限列表
                List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
                Collection<GrantedAuthority> authority = new ArrayList<>();
                for(String permissionValue : permissionValueList) {
                    SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                    authority.add(auth);
                }*/
                List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(authRoles));
    //            return new UsernamePasswordAuthenticationToken(tokenMap.get("username"),token,authority);
                // 已经通过令牌认证成功,密码设置为空即可
                User user = new User(username,"",authorityList);
                return new UsernamePasswordAuthenticationToken(user,token,authorityList);
            }
            return null;
        }
    }
    
  7. 登录过滤器

    package com.wyx.SpringSecurityConfig;
    
    
    import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper;
    import com.wyx.SpringSecurityConfig.pojo.SecurityUser;
    import com.wyx.utils.PasswordEncryption;
    import com.wyx.utils.TokenManager;
    import lombok.extern.java.Log;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    @Log
    public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    
        private final RedisTemplate redisTemplate;
    
        private final SecurityUserMapper securityUserMapper;
    
        private final AuthenticationManager authenticationManager;
    
    
        public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate, SecurityUserMapper securityUserMapper) {
            this.redisTemplate = redisTemplate;
            this.authenticationManager = authenticationManager;
            this.securityUserMapper = securityUserMapper;
            this.setPostOnly(false);
            this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
        }
    
    
        //1 获取表单提交用户名和密码
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            //获取表单提交数据
            //使用流方式获取表单信息
            //Users user = new ObjectMapper().readValue(request.getInputStream(), Users.class);
            //User users = new User(request.getParameter("username"), request.getParameter("password"));
            // 设置盐,从数据库中获取
            SecurityUser user = securityUserMapper.getSecurityUserByUserName(request.getParameter("username"));
            // 将密码修改为用户输入的
            user.setPassword(request.getParameter("password"));
            // 将加密的盐保存到请求中
            request.setAttribute("loginSalt",user.getSalt());
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),
                    PasswordEncryption.encryption(user.getPassword(),user.getSalt()),
                    new ArrayList<>()));
        }
        //2 认证成功调用的方法
        @Override
        protected void successfulAuthentication(HttpServletRequest request,
                                                HttpServletResponse response, FilterChain chain, Authentication authResult) {
            //认证成功,得到认证成功之后用户信息
            User user = (User) authResult.getPrincipal();
            //查询数据库获取用户ID 用户名,用户权限。
            String username = user.getUsername();
    
            StringBuilder roles = new StringBuilder();
            for (GrantedAuthority authority : authResult.getAuthorities()) {
                roles.append(authority);
                roles.append(",");
            }
            // 消除最后一个分号
            if (roles.length()>=1){
                roles.replace(roles.length()-1,roles.length(),"");
            }
            String token = TokenManager.encryption(Long.valueOf(new Random().nextInt()),username, String.valueOf(roles));
            //把用户名称和用户权限列表放到redis,并设置过期时间
            HashMap tokenMap = TokenManager.jwtDecrypt(token);
            Date date = (Date) tokenMap.get("expireTime");
            redisTemplate.opsForValue().set(username,token,(date.getTime()-new Date().getTime())/1000, TimeUnit.SECONDS);
            //返回token
            response.setHeader("token",token);
    
        }
    
        //3 认证失败调用的方法
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws ServletException, IOException {
            request.getRequestDispatcher("/authority/AuthenticationFailed").forward(request,response);
            log.warning("认证失败,请重新认证");
        }
    }
    
  8. 退出登录处理器

    package com.wyx.SpringSecurityConfig;
    
    import com.wyx.utils.TokenManager;
    import lombok.extern.java.Log;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    
    //退出处理器
    @Log
    public class TokenLogoutHandler implements LogoutHandler {
    
    
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    
            // 功能较为简单,只要请求头包含token就能退出,后期根据自己的业务需求更改
    
            String token = request.getHeader("token");
            if(token != null) {
    
                // 从 token中获取用户名
                HashMap tokenMap = TokenManager.jwtDecrypt(token);
                try {
                    response.sendRedirect("/authority/logoutOk");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                log.info("用户退出登录成功");
    
                // 在 redis 上删除token,可以删除,也可以不删。
    //            redisTemplate.delete(tokenMap.get("username"));
            }else {
                try {
                    request.getRequestDispatcher("/authority/logoutEr").forward(request,response);
                } catch (ServletException | IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  9. 获取数据库用户信息处理器

    package com.wyx.SpringSecurityConfig;
    
    import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper;
    import com.wyx.SpringSecurityConfig.pojo.SecurityUser;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    @Service("userDetailsService")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Resource
        SecurityUserMapper securityUserMapper;
    
    
        @Autowired
        MyPasswordEncoder myPasswordEncoder;
    
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            SecurityUser user = securityUserMapper.getSecurityUserByUserName(username);
    
            List<String> roleListByUserName = securityUserMapper.getRoleListByUserName(username);
    
            // 设置权限,通过逗号分割
            StringBuilder role = new StringBuilder();
            for (String s : roleListByUserName) {
                role.append("ROLE_"+s);
                role.append(",");
            }
            //消除最后一个分号
            if (role.length()>=1){
                role.replace(role.length()-1,role.length(),"");
            }else {
                role.append("");
            }
    
            List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(role));
            SecurityUser securityUser = new SecurityUser(user.getId(),user.getUsername(),
                    user.getPassword(),user.getSalt());
    
            if (securityUser.getUsername()==null){
                throw  new UsernameNotFoundException("用户名不存在!");
            }
    
            return new User(securityUser.getUsername(),securityUser.getPassword(),authorityList);
    
        }
    }
    
  10. 安全框架总配置

    package com.wyx.SpringSecurityConfig;
    
    import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import javax.annotation.Resource;
    
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private MyPasswordEncoder myPasswordEncoder;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Resource
        SecurityUserMapper securityUserMapper;
    
        /**
         * 配置设置
         * @param http
         * @throws Exception
         */
        //设置退出的地址和token,redis操作地址
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.exceptionHandling()
                    .authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问处理器
                    .and().csrf().disable()
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and().logout().logoutUrl("/logout").permitAll()//退出路径
                    .addLogoutHandler(new TokenLogoutHandler()).and()       //退出登录处理器
                    .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate, securityUserMapper))    //登录处理器 
                    .addFilter(new TokenAuthFilter(authenticationManager(),redisTemplate)).httpBasic();     //授权处理器
        }
    
        //调用userDetailsService和密码处理
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(myPasswordEncoder);
        }
    
        //不进行认证的路径,可以直接访问
        @Override
        public void configure(WebSecurity web) throws Exception {
            // 配置过了swagger ui 的过滤器
            web.ignoring().antMatchers(
                	"/error",
                    "/error/**",
                    "/authority/logoutOk",
                    "/doc.html","/webjars/**",
                    "/v2/api-docs",
                    "/swagger-resources",
                    "/swagger-resources/**",
                    "/configuration/ui",
                    "/configuration/security",
                    "/swagger-ui.html/**",
                    "/csrf","/"
            );
        }
    }
    
  11. 权限信息返回控制器

    package com.wyx.SpringSecurityConfig.controller;
    
    import com.wyx.utils.CommonResult;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * @ClassName AuthorityController
     * @Description TODO
     * @Author 王玉星
     * @Date 2021/8/7 18:10
     * @Version 1.0
     */
    @RestController
    @RequestMapping("/authority")
    public class AuthorityController {
    
        // 无权限访问响应
        @RequestMapping("/noAuthority")
        public CommonResult noAuthority(){
            return CommonResult.perNoAll();
        }
    
        // 认证失败响应
        @RequestMapping("/AuthenticationFailed")
        public CommonResult AuthenticationFailed(){
            return CommonResult.MyMessage("认证失败,用户名或密码错误");
        }
    
    
        // 退出成功处理器
        @RequestMapping("/logoutOk")
        public CommonResult logoutOk(HttpServletResponse response){
            // 给前端一个空的请求头,让前端将请求头参数设置为空即可
            response.setHeader("token","");
            return CommonResult.Success("退出登录成功

分类:

技术点:

相关文章: