您现在的位置是:首页 >技术教程 >SpringSecurity实现前后端分离认证授权网站首页技术教程

SpringSecurity实现前后端分离认证授权

我也有梦想呀 2023-05-15 04:00:03
简介SpringSecurity实现前后端分离认证授权

1、什么是SpringSecurity?

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求

从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。

怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。

Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

认证(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

授权(Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

2、先浅浅的来一个SpringSecurity入门

先创建一个普通的SpringBoot工程

<dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--security-->
        <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>-->
    </dependencies>

编写一个请求

package com.ly.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  21:55
 * @tags 喜欢就去努力的争取
 */
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

结果:正常访问,这简单不过了
image

整合Security呢?我们把前面pom.xml中的依赖包放开,刷新一下maven重启项目
image

再来访问一下我们的接口:http://localhost:8080/login
image

可以发现Security给我们拦下来了,需要我们做认证:输入用户名user,密码:控制台复制
image

登录进去后,就又可以访问我们的接口了
image

以上就是一个简单的SpringBoot整合SpringSecurity的demo;下面将堆SpringSecurity安全认证框架进行详细的拆解描述

3、登录认证流程

image
注意:虽然SpringSecurity帮我们做了很多事情,但是很多东西是需要我们根据各自的系统进行定制的,如下:
1.登录页面是需要我们自己定义的
2.用户名和密码也应该是我们数据的用户名和密码
3.token我们怎么生成
4.token我们怎么解析
5.用户授权

认证详细流程
image

4、具体的实现思路

认证
image

  • 1.自定义登录接口:调用providerManager进行认证,认证通过把用户信息存入Redis【key:userid,value:用户信息】;使用jwt生成token
  • 2.自定义UserDetailService:查询数据库

校验
image

  • 1.自定义jwt校验的过滤器:请求头中获取token —> 解析token拿到用户的信息 —> 去redis中获取完整的用户信息 —> 把完整的用户信息存入SecurityContextHolder供后续的过滤器使用

5、准备工作

(1)改造我们前面创建的快速入门工程;修改pom.xml
<?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.ly.security</groupId>
    <artifactId>security_quick_start</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--jjwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
		
	<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
    </dependencies>
</project>
(2) 统一响应枚举 ResponseEnum
package com.ly.security.common;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  23:48
 * @tags 喜欢就去努力的争取
 */
@Getter
@AllArgsConstructor
@ToString
public enum ResponseEnum {

    SUCCESS(20000, "成功"),
    ERROR(-1, "服务器内部错误"),

    //2xx 参数校验
    PASSWORD_NULL_ERROR(204, "密码不能为空"),
    LOGIN_PASSWORD_ERROR(209, "密码错误"),
    LOGIN_LOKED_ERROR(210, "用户被锁定"),
    LOGIN_AUTH_ERROR(211, "未登录"),
    ;

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 消息
     */
    private String message;
}

(3) 统一结果集返回 R
package com.ly.security.common;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  23:46
 * @tags 喜欢就去努力的争取
 */
@Data
public class R {

    private Integer code;

    private String message;

    private Map<String, Object> data = new HashMap();

    /**
     * 构造器私有
     */
    private R() {
    }

    /**
     * 返回成功
     */
    public static R ok() {
        R r = new R();
        r.setCode(ResponseEnum.SUCCESS.getCode());
        r.setMessage(ResponseEnum.SUCCESS.getMessage());
        return r;
    }

    /**
     * 返回失败
     */
    public static R error() {
        R r = new R();
        r.setCode(ResponseEnum.ERROR.getCode());
        r.setMessage(ResponseEnum.ERROR.getMessage());
        return r;
    }

    /**
     * 设置特定结果
     */
    public static R setResult(ResponseEnum responseEnum) {
        R r = new R();
        r.setCode(responseEnum.getCode());
        r.setMessage(responseEnum.getMessage());
        return r;
    }

    public R message(String message) {
        this.setMessage(message);
        return this;
    }

    public R code(Integer code) {
        this.setCode(code);
        return this;
    }

    public R data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }

    public R data(Map<String, Object> map) {
        this.setData(map);
        return this;
    }
}
(4)Jwt工具类
package com.ly.security.utils;

import com.ly.security.exception.BusinessException;
import io.jsonwebtoken.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  23:53
 * @tags 喜欢就去努力的争取
 */
public class JwtUtils {

    private static long tokenExpiration = 24 * 60 * 60 * 1000;
    private static String tokenSignKey = "gek^/)_!e_5[(Spa[rAdc9Roby?JJs";

    private static Key getKeyInstance() {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        byte[] bytes = Base64Utils.encode(tokenSignKey.getBytes());
        return new SecretKeySpec(bytes, signatureAlgorithm.getJcaName());
    }

    /**
     * 创建token
     *
     * @param id
     * @param username
     * @return
     */
    public static String createToken(Long id, String username) {
        return Jwts.builder()
                .setSubject("ly")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("id", id)
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, getKeyInstance())
                .compressWith(CompressionCodecs.GZIP)
                .compact();
    }

    /**
     * 创建token
     *
     * @param id
     * @return
     */
    public static String createToken(String id) {
        return Jwts.builder()
                .setSubject("ly")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("id", id)
                .signWith(SignatureAlgorithm.HS512, getKeyInstance())
                .compressWith(CompressionCodecs.GZIP)
                .compact();
    }

    /**
     * 判断token是否有效
     *
     * @param token
     * @return
     */
    public static boolean checkToken(String token) {
        if (StringUtils.isBlank(token)) {
            return false;
        }
        try {
            Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public static Long getUserId(String token) {
        Claims claims = getClaims(token);
        Long id = (Long) claims.get("id");
        return id;
    }

    public static String getUsername(String token) {
        Claims claims = getClaims(token);
        return (String) claims.get("username");
    }

    public static void removeToken(String token) {
        //jwttoken无需删除,客户端扔掉即可。
    }

    /**
     * 校验token并返回Claims
     *
     * @param token
     * @return
     */
    private static Claims getClaims(String token) {
        if (StringUtils.isEmpty(token)) {
            // LOGIN_AUTH_ERROR(-211, "未登录"),
            throw new BusinessException("未登录");
        }
        try {
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
            return claimsJws.getBody();
        } catch (Exception e) {
            throw new BusinessException("未登录");
        }
    }

    public static void main(String[] args) {
        String token = createToken("ly");
        System.out.println("token = " + token);
        boolean flag = checkToken(token);
        Claims claims = getClaims(token);
        String subject = claims.getSubject();
        System.out.println("subject = " + subject);
    }
}
(5) redis工具类
package com.ly.security.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  23:54
 * @tags 喜欢就去努力的争取
 */
@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public void deleteObject(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection
     */
    public void deleteObject(Collection collection) {
        redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList) {
        ListOperations listOperation = redisTemplate.opsForList();
        if (null != dataList) {
            int size = dataList.size();
            for (int i = 0; i < size; i++) {
                listOperation.leftPush(key, dataList.get(i));
            }
        }
        return listOperation;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(String key) {
        List<T> dataList = new ArrayList<T>();
        ListOperations<String, T> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);

        for (int i = 0; i < size; i++) {
            dataList.add(listOperation.index(key, i));
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(String key) {
        Set<T> dataSet = new HashSet<T>();
        BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     * @return
     */
    public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (null != dataMap) {
            for (Map.Entry<String, T> entry : dataMap.entrySet()) {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(String key) {
        Map<String, T> map = redisTemplate.opsForHash().entries(key);
        return map;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}
(6) WebUtils工具类,针对前后端分离写回JSON数据
package com.ly.security.utils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-07  0:14
 * @tags 喜欢就去努力的争取
 */
public class WebUtils {

    /**
     * @param response 渲染对象
     * @param str      带渲染字符串
     * @return
     */
    public static String renderString(HttpServletResponse response, String str) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().write(str);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}
(7)统一异常
package com.ly.security.exception;

import com.ly.security.common.ResponseEnum;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-06  23:58
 * @tags 喜欢就去努力的争取
 */
@Data
@NoArgsConstructor
public class BusinessException extends RuntimeException {

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 错误消息
     */
    private String message;

    /**
     *
     * @param message 错误消息
     */
    public BusinessException(String message) {
        this.message = message;
    }

    /**
     *
     * @param message 错误消息
     * @param code 错误码
     */
    public BusinessException(String message, Integer code) {
        this.message = message;
        this.code = code;
    }

    /**
     *
     * @param message 错误消息
     * @param code 错误码
     * @param cause 原始异常对象
     */
    public BusinessException(String message, Integer code, Throwable cause) {
        super(cause);
        this.message = message;
        this.code = code;
    }

    /**
     *
     * @param resultCodeEnum 接收枚举类型
     */
    public BusinessException(ResponseEnum resultCodeEnum) {
        this.message = resultCodeEnum.getMessage();
        this.code = resultCodeEnum.getCode();
    }

    /**
     *
     * @param resultCodeEnum 接收枚举类型
     * @param cause 原始异常对象
     */
    public BusinessException(ResponseEnum resultCodeEnum, Throwable cause) {
        super(cause);
        this.message = resultCodeEnum.getMessage();
        this.code = resultCodeEnum.getCode();
    }
}
(8)统一异常处理
package com.ly.security.exception;

import com.ly.security.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-07  0:28
 * @tags 喜欢就去努力的争取
 */
@Slf4j
@Component
@RestControllerAdvice // 在controller层添加通知。如果使用@ControllerAdvice,则方法上需要添加@ResponseBody
public class UnifiedExceptionHandler {

    @ExceptionHandler(value = AccessDeniedException.class)
    public R handleAccessDeniedException(AccessDeniedException e) {
        log.error(e.getMessage(), e);
        return R.error().code(-1).message(e.getMessage());
    }

    /**
     * 自定义异常处理
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = BusinessException.class)
    public R handleBusinessException(BusinessException e) {
        log.error(e.getMessage(), e);
        return R.error().code(e.getCode()).message(e.getMessage());
    }

    /**
     * Controller上一层相关异常
     */
    @ExceptionHandler({
            NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            MethodArgumentNotValidException.class,
            HttpMediaTypeNotAcceptableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    public R handleServletException(Exception e) {
        log.error(e.getMessage(), e);
        //SERVLET_ERROR(-102, "servlet请求异常"),
        return R.error().message("servlet请求异常").code(-102);
    }

    /**
     * 未定义异常
     * 当controller中抛出Exception,则捕获
     */
    @ExceptionHandler(value = Exception.class)
    public R handleException(Exception e) {
        log.error(e.getMessage(), e);
        return R.error();
    }
}
(9)数据库表
/*
 Navicat MySQL Data Transfer

 Source Server         : startqbb
 Source Server Type    : MySQL
 Source Server Version : 80029
 Source Host           : localhost:3306
 Source Schema         : ly_auth

 Target Server Type    : MySQL
 Target Server Version : 80029
 File Encoding         : 65001

 Date: 07/04/2023 00:43:03
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `parent_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '所属上级',
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '名称',
  `type` tinyint(0) NOT NULL DEFAULT 0 COMMENT '类型(0:目录,1:菜单,2:按钮)',
  `path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址',
  `component` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
  `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标',
  `sort_value` int(0) NULL DEFAULT NULL COMMENT '排序',
  `status` tinyint(0) NULL DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_parent_id`(`parent_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '角色id',
  `role_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '角色名称',
  `role_code` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色编码',
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(0) NOT NULL DEFAULT 0,
  `menu_id` bigint(0) NOT NULL DEFAULT 0,
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_role_id`(`role_id`) USING BTREE,
  INDEX `idx_menu_id`(`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色菜单' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '会员id',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机',
  `head_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像地址',
  `dept_id` bigint(0) NULL DEFAULT NULL COMMENT '部门id',
  `post_id` bigint(0) NULL DEFAULT NULL COMMENT '岗位id',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
  `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态(1:正常 0:停用)',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `role_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '角色id',
  `user_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '用户id',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_role_id`(`role_id`) USING BTREE,
  INDEX `idx_admin_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

(10) 实体类

image
SysMenu:菜单

package com.ly.security.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;

import java.util.List;
/**
 * 菜单
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
@TableName("sys_menu")
public class SysMenu extends BaseEntity {
	
	private static final long serialVersionUID = 1L;

	/**
	 * 所属上级
	 */
	@TableField("parent_id")
	private Long parentId;

	/**
	 * 名称
	 */
	@TableField("name")
	private String name;

	/**
	 * 类型(1:菜单,2:按钮)
	 */
	@TableField("type")
	private Integer type;

	/**
	 * 路由地址
	 */
	@TableField("path")
	private String path;

	/**
	 * 组件路径
	 */
	@TableField("component")
	private String component;

	/**
	 * 权限标识
	 */
	@TableField("perms")
	private String perms;

	/**
	 * 图标
	 */
	@TableField("icon")
	private String icon;

	/**
	 * 排序
	 */
	@TableField("sort_value")
	private Integer sortValue;

	/**
	 * 状态(0:禁止,1:正常)
	 */
	@TableField("status")
	private Integer status;

	/**
	 * 下级列表
	 */
	@TableField(exist = false)
	private List<SysMenu> children;

	/**
	 * 是否选中
	 */
	@TableField(exist = false)
	private boolean isSelect;
}

SysRole:角色

package com.ly.security.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;

/**
 * 角色
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
@TableName("sys_role")
public class SysRole extends BaseEntity {
	
	private static final long serialVersionUID = 1L;

	/**
	 * 角色名
	 */
	@TableField("role_name")
	private String roleName;

	/**
	 * 角色编码
	 */
	@TableField("role_code")
	private String roleCode;

	/**
	 * 描述
	 */
	@TableField("description")
	private String description;

}

SysRoleMenu:角色菜单

package com.ly.security.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;

/**
 * 角色菜单
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
@TableName("sys_role_menu")
public class SysRoleMenu extends BaseEntity {

    private static final long serialVersionUID = 1L;

	/**
	 * 角色id
	 */
    @TableField("role_id")
    private String roleId;

	/**
	 * 菜单id
	 */
    @TableField("menu_id")
    private String menuId;

}

SysUser:用户

package com.ly.security.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;

import java.util.List;

/**
 * 用户
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
@TableName("sys_user")
public class SysUser extends BaseEntity {

    private static final long serialVersionUID = 1L;

	/**
	 * 用户名
	 */
    @TableField("username")
    private String username;

	/**
	 * 密码
	 */
    @TableField("password")
    private String password;

	/**
	 * 姓名
	 */
    @TableField("name")
    private String name;

	/**
	 * 手机
	 */
    @TableField("phone")
    private String phone;

	/**
	 * 头像地址
	 */
    @TableField("head_url")
    private String headUrl;

	/**
	 * 部门id
	 */
    @TableField("dept_id")
    private Long deptId;

	/**
	 * 岗位id
	 */
    @TableField("post_id")
    private Long postId;

	/**
	 * 描述
	 */
    @TableField("description")
    private String description;

	/**
	 * 状态(1:正常 0:停用)
	 */
    @TableField("status")
    private Integer status;

    @TableField(exist = false)
    private List<SysRole> roleList;

	/**
	 * 岗位
	 */
	@TableField(exist = false)
    private String postName;

	/**
	 * 部门
	 */
	@TableField(exist = false)
    private String deptName;
}

SysUserRole:用户角色

package com.ly.security.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;

/**
 * 用户角色
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
@TableName("sys_user_role")
public class SysUserRole extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 角色id
     */
    @TableField("role_id")
    private String roleId;

    /**
     * 用户id
     */
    @TableField("user_id")
    private String userId;
}

baseEntity:通用属性

package com.ly.security.domain.base;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-05  22:14
 * @tags 喜欢就去努力的争取
 */
@Data
public class BaseEntity implements Serializable {

    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    @TableLogic  //逻辑删除 默认效果 0 没有删除 1 已经删除
    @TableField("is_deleted")
    private Integer isDeleted;

    @TableField(exist = false)
    private Map<String, Object> param = new HashMap<>();
}
(11) mybatis-plus自动填充
package com.ly.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-01  23:04
 * @tags 喜欢就去努力的争取
 */
@Component
public class CommonMetaObjectHandler 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);
    }
}
(12) 断言类Assert
package com.ly.security.common;

import com.ly.security.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.function.Supplier;

/**
 * @author l&y (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2022-10-16 12:43
 * @tags 我爱的人在很远的地方,我必须更加努力
 */
@Slf4j
public abstract class Assert {

    /**
     * 断言对象不为空
     * 如果对象obj为空,则抛出异常
     *
     * @param obj 待判断对象
     */
    public static void notNull(Object obj, ResponseEnum responseEnum) {
        if (obj == null) {
            log.info("obj is null...............");
            throw new BusinessException(responseEnum);
        }
    }


    /**
     * 断言对象为空
     * 如果对象obj不为空,则抛出异常
     *
     * @param object
     * @param responseEnum
     */
    public static void isNull(Object object, ResponseEnum responseEnum) {
        if (object != null) {
            log.info("obj is not null......");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言表达式为真
     * 如果不为真,则抛出异常
     *
     * @param expression 是否成功
     */
    public static void isTrue(boolean expression, ResponseEnum responseEnum) {
        if (!expression) {
            log.info("fail...............");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言表达式为假
     * 如果不为真,则抛出异常
     *
     * @param expression 是否成功
     */
    public static void isFalse(boolean expression, ResponseEnum responseEnum) {
        if (expression) {
            log.info("fail...............");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言两个对象不相等
     * 如果相等,则抛出异常
     *
     * @param m1
     * @param m2
     * @param responseEnum
     */
    public static void notEquals(Object m1, Object m2, ResponseEnum responseEnum) {
        if (m1.equals(m2)) {
            log.info("equals...............");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言两个对象相等
     * 如果不相等,则抛出异常
     *
     * @param m1
     * @param m2
     * @param responseEnum
     */
    public static void equals(Object m1, Object m2, ResponseEnum responseEnum) {
        if (!m1.equals(m2)) {
            log.info("not equals...............");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言参数不为空
     * 如果为空,则抛出异常
     *
     * @param s
     * @param responseEnum
     */
    public static void notEmpty(String s, ResponseEnum responseEnum) {
        if (StringUtils.isEmpty(s)) {
            log.info("is empty...............");
            throw new BusinessException(responseEnum);
        }
    }

    /**
     * 断言参数不为空
     * 如果为空,则抛出异常
     *
     * @param list
     * @param responseEnum
     */
    public static void notEmpty(List<Long> list, ResponseEnum responseEnum) {
        if (list == null || list.isEmpty()) {
            log.info("is empty...............");
            throw new BusinessException(responseEnum);
        }
    }

    public static void custom(Supplier<Boolean> supplier, ResponseEnum responseEnum) {
        if (supplier.get()) {
            log.info("custom method error...............");
            throw new BusinessException(responseEnum);
        }
    }
}

6、代码实现

(1)根据用户名从数据库中查询用户以及用户的权限信息
package com.ly.security.auth;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.domain.SysUser;
import com.ly.security.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-07  22:30
 * @tags 喜欢就去努力的争取
 */
@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    /**
     * 根据用户名查询用户和用户的权限信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(StringUtils.isNotBlank(username), SysUser::getUsername, username));
        Assert.notNull(sysUser, ResponseEnum.USER_NOT_EXIST);
        // TODO 获取用户授权信息

        // 封装为UserDetails对象返回
        return new LoginUser(sysUser);
    }
}

注意:我们的在这个loadUsername()方法,底层会由AuthenticationManager认证管理器进行自动调用

(2)把查询出来的用户信息封装和权限信息封装成UserDetails对象

我们创建一个LoginUser类是吸纳UserDetails接口

package com.ly.security.auth;

import com.ly.security.domain.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-07  22:53
 * @tags 喜欢就去努力的争取
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser sysUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return Boolean.TRUE;
    }

    @Override
    public boolean isAccountNonLocked() {
        return Boolean.TRUE;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return Boolean.TRUE;
    }

    @Override
    public boolean isEnabled() {
        return Boolean.TRUE;
    }
}
(3)自定义密码加密方式
package com.ly.security.auth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * security配置类
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-09  15:07
 * @tags 喜欢就去努力的争取
 */
@Configuration
@EnableWebSecurity
public class LySecurityConfig {

    /**
     * 密码加密器,会把客户端传来的密码进行加密,然后跟数据库中的密码做对比,要求数据库中的密码也是加密过的
     * 如果没有该加密器,spring security会报出异常:There is no PasswordEncoder mapped for the id "null"
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
(4)编写一个login登录接口
package com.ly.security.controller;

import com.ly.security.common.R;
import com.ly.security.domain.vo.SysUserVo;
import com.ly.security.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import static com.ly.security.common.LyConstant.LOGIN_MSG;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-09  15:33
 * @tags 喜欢就去努力的争取
 */
@RestController
public class LoginController {

    @Autowired
    private SysUserService sysUserService;

    @PostMapping("/user/login")
    public R login(@RequestBody SysUserVo sysUserVo){
        sysUserService.login(sysUserVo);
        return R.ok().message(LOGIN_MSG);
    }
}
(5)SysUserServiceImpl实现类
package com.ly.security.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ly.security.auth.LoginUser;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.domain.SysUser;
import com.ly.security.domain.vo.SysUserVo;
import com.ly.security.mapper.SysUserMapper;
import com.ly.security.service.SysUserService;
import com.ly.security.utils.JwtUtils;
import com.ly.security.utils.RedisCache;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

import static com.ly.security.common.LyConstant.LOGIN_USER_CACHE_KEY;

/**
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-03-10  20:15
 * @tags 喜欢就去努力的争取
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    private final AuthenticationManager authenticationManager;
    private final RedisCache redisCache;

    public SysUserServiceImpl(AuthenticationManager authenticationManager, RedisCache redisCache) {
        this.authenticationManager = authenticationManager;
        this.redisCache = redisCache;
    }

    @Override
    public String login(SysUserVo sysUserVo) {
        // 使用AuthenticationManager.authenticate进行用户认证 ---> 会去调用我们的UserDetailServiceImpl的loadUserByUsername进行认证
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(sysUserVo.getUsername(), sysUserVo.getPassword()));
        // 如果认证没通过,给出提示
        Assert.notNull(authenticate, ResponseEnum.LOGIN_ERROR);
        // 认证通过了,使用用户信息生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        Long userId = loginUser.getSysUser().getId();
        String token = JwtUtils.createToken(userId.toString());
        // 把用户信息存入redis
        redisCache.setCacheObject(LOGIN_USER_CACHE_KEY + userId, token, 1, TimeUnit.DAYS);
        // 返回token
        return token;
    }
}
(6) Security核心配置类
package com.ly.security.auth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 * security配置类
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-09  15:07
 * @tags 喜欢就去努力的争取
 */
@Configuration
@EnableWebSecurity
public class LySecurityConfig {

    /**
     * 密码加密器,会把客户端传来的密码进行加密,然后跟数据库中的密码做对比,要求数据库中的密码也是加密过的
     * 如果没有该加密器,spring security会报出异常:There is no PasswordEncoder mapped for the id "null"
     * 拓展一下,这里也可以自定义密码加密方式,只需要自定义一个xxxPasswordEncoder 实现 PasswordEncoder 接口
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     *
     * @param authConfig
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    /**
     * Security核心配置
     *
     * @param security
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity security) throws Exception {

        return security
                // 关闭csrf
                .csrf().disable()
                // 基于 token,不需要 session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 登录接口肯定是不需要认证的
                .antMatchers("/user/login").anonymous()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and().build();
    }
}
(7)配置文件
server:
  port: 9001
spring:
  application:
    name: ly-security
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ly_auth?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  # 开启循环依赖
  main:
    allow-circular-references: true
  # 配置日期格式化
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  redis:
    host: 你的IP
    port: 6379
    timeout: 30000
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1
        min-idle: 0
(8) 测试

image

image

7、token认证过滤器

(1) 自定义token认真过滤器
package com.ly.security.filter;

import com.ly.security.auth.LoginUser;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.JwtUtils;
import com.ly.security.utils.RedisCache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static com.ly.security.common.LyConstant.LOGIN_USER_CACHE_KEY;
import static com.ly.security.common.LyConstant.TOKEN_KEY;

/**
 * 自定义token过滤器
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-11  23:25
 * @tags 喜欢就去努力的争取
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private RedisCache redisCache;

    public JwtAuthenticationTokenFilter(RedisCache redisCache) {
        this.redisCache = redisCache;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            // token为空直接放行
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        Long userId = JwtUtils.getUserId(token);

        // 拿着userId去Redis中获取用户信息
        String cacheKey = LOGIN_USER_CACHE_KEY + userId;
        LoginUser userInfo = redisCache.getCacheObject(cacheKey);
        Assert.notNull(userInfo, ResponseEnum.LOGIN_AUTH_ERROR);

        // 把用户信息存入SecurityContextHolder
        // TODO 权限信息还未封装进去
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userInfo, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 放行
        filterChain.doFilter(request, response);
    }
}

(2)配置token认证过滤器
......

.addFilterBefore(new JwtAuthenticationTokenFilter(redisCache), UsernamePasswordAuthenticationFilter.class)

......

image

8、退出登录

删除redis中的用户信息即可

@Override
    public void logout() {
        // 获取SecurityContextHolder中的用户信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 删除redis中的用户信息
        Long id = loginUser.getSysUser().getId();
        String cacheKey = LOGIN_USER_CACHE_KEY + id;
        redisCache.deleteObject(cacheKey);
    }

9、授权

我们需要查询出指定用户的权限

SELECT
	DISTINCT sm.perms 
FROM
	sys_menu sm
	LEFT JOIN sys_role_menu srm ON sm.id = srm.menu_id
	LEFT JOIN sys_role sr ON sr.id = srm.role_id
	LEFT JOIN sys_user_role sur ON sr.id = sur.role_id
	LEFT JOIN sys_user su ON su.id = sur.user_id 
WHERE
	su.id = 1 
	AND sm.type = 2
	AND su.`status` = 1 
	AND su.is_deleted = 0;

测试权限控制
UserDetailServiceImpl

List<String> permissions = sysMenuService.getPermissionsByUserId(sysUser.getId());

SysMenuServiceImpl

 @Override
    public List<String> getPermissionsByUserId(Long userId) {
        Assert.notNull(userId, ResponseEnum.USER_NOT_EXIST);
        QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
        wrapper.eq("sur.user_id", userId)
                .eq("sm.`status`", USER_STATUS)
                .eq("sm.type", ENUM_TYPE)
                .eq("srm.is_deleted", IS_DELETED_ZERO)
                .eq("sm.is_deleted", IS_DELETED_ZERO)
                .eq("sm.is_deleted", IS_DELETED_ZERO);
        return sysMenuMapper.getPermissionsByUserId(wrapper);
    }

SysMenuMapper

@Select("SELECT DISTINCT sm.perms  FROM sys_menu sm INNER JOIN sys_role_menu srm ON sm.id = srm.menu_id INNER JOIN sys_user_role sur ON sur.role_id = srm.role_id ${ew.customSqlSegment}")
    List<String> getPermissionsByUserId(@Param(Constants.WRAPPER) Wrapper<SysMenu> wrapper);

10、认证失败处理器

package com.ly.security.handler;

import com.ly.security.common.R;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.WebUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证失败处理器
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-13  23:58
 * @tags 喜欢就去努力的争取
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        R r = R.setResult(ResponseEnum.UNAUTHORIZED);
        WebUtils.renderString(response, r.toString());
    }
}

别忘了在Security核心配置类配置一下
image

11、授权失败处理器

package com.ly.security.handler;

import com.ly.security.common.R;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 授权失败处理器
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-14  0:03
 * @tags 喜欢就去努力的争取
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        R r = R.setResult(ResponseEnum.FORBIDDEN);
        WebUtils.renderString(response, r.toString());
    }
}

别忘了在Security核心配置类配置一下
image

12、跨域配置

package com.ly.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域配置
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-14  0:03
 * @tags 喜欢就去努力的争取
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允诈跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

别忘了在Security核心配置类配置一下
image

13、hasAuthority()的原理

@PreAuthorize("hasAuthority('admin')"):hasAuthority()中的我们在方法加的这段注解实际上是SPEL的一个表达式,Spring框架会替我们去解析它
所以我么点进方法看一看原理
image

  • hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源.
  • hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_这个前缀才可以。
  • hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_这个前才可以。

14、自定义权限校验

前面我们大致的知道了权限校验的底层原理,现在我们就可以自定义一个权限校验的方法

package com.ly.security.expression;

import com.ly.security.auth.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 自定义权限控制
 *
 * @author ly (个人博客:https://www.cnblogs.com/qbbit)
 * @date 2023-04-15  20:57
 * @tags 喜欢就去努力的争取
 */
@Component("ex")
public class LyExpression {

    public boolean hasAuthority(String authority) {
        // 获取当前用户的权限列表
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        // 判断权限
        return permissions.contains(authority);
    }

}

使用SPEL调用我们自定义的权限校验方法
image

15、基于配置的方式实现权限控制

修改Security核心配置类
image

16、CSRF

  • SpringSecurity去防止CSRF攻击的方式就是通过csrf token。后端会生成一个csrf token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
  • 我们可以发现CSRF攻击依靠的cookie中所洪带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
    image

最后回顾一下整个流程图
image

推荐观看Bilibili:三更大佬的视频

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。