三更的springsecurity课程个人笔记总计4万字,全部测试通过,代码cv即可

SpringSecurity

b站

40.源码讲解部分说明_哔哩哔哩_bilibili

BV1mm4y1X7Hc

以下全为个人总结,不能代表官方,有错误还请指出(全部测试通过)

1-简介

tip

接下来的所有类不会包含import信息,请在设置里开启自动导包功能

image-20240806113335479

java框架

身份验证,授权,防止攻击

基于过滤器链,可集成到Spring应用

举例
认证:回家时,房子里的人问你是谁,得知你是其中的成员后则可进入

授权:进到屋子里,只有权限躺地上睡觉,没有其他使用电视,wifi的权限

2-入门案例

新建空项目-新建maven模块

image-20240805094919708

加入依赖+boot父工程

 <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.5.0</version>
  </parent>
  <dependencies>
    <!--security依赖项目(重点)-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

创建启动类

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SecurityApplication.class);
    }
}

创建测试类Hello

@RestController
public class HelloController {

    @GetMapping
    public String hello(){
        return "Hello World!";
    }
}

浏览器访问测试

http://localhost:8080/

image-20240805103538206

自动跳转登录页面

用户名为user

密码为

image-20240805103606268

image-20240805103641118

注销url

Confirm Log Out?

image-20240805103711083

3-认证

3.1-登录校验流程

image-20240805104318866

  1. 输入密码账号

  2. 后端拿用户名和密码比对数据库

  3. 正确则生成token给前端

  4. 后续前端所有请求头携带该token

  5. 后端根据token判断是否有权限,有放行响应结果给前端,没有则拦截

输密码-查库-造token-每次访问查token

3.2-完整流程

image-20240805105152113

过滤器链

认证——UsernamePasswordAuthenticationFilter

授权——FilterSecurityInterceptor

异常处理——Exception…

默认登录/注销页

image-20240805105402153

查看springsecurity默认过滤器链

使用idea评估器查看容器内的bean

上到下依次执行

3.3-案例认证流程

image-20240805111631906
以下的UsernamePasswordAuthenticationFilter统称为UPAF

Authentication统称为A
AuthenticationManager为AM (图例ProviderManager的实现类)

upaf过滤器链里封装了对象A, 存储 用户名和密码信息(从前端传来的user解构进去的)

调用 AM上的authenticate方法认证形参为A,最后调用userdetailsService的loaduser方法…,根据用户名从内存或是库里获取出来

获取到的用户信息userdetails也就是loaduser…方法的放回值,将前台输入的密码和根据用户名查到的密码通过encoder编码后进行比对

正确则给a 增加权限信息,目前拥有用户名和密码信息和权限信息

并将该结果对象作为认证方法的返回值返回给upaf
在这里插入图片描述

前台输入的账号密码

调用方法开始认证

库里根据账号查出密码

编码后比对,正确则添加权限信息并返回对象(uname,pwd,permission权限集合)

对象存到security容器里 (securityContextHolder…)

如何DIY?

还记得我们一开始访问的登陆页面吗,使用的密码是随机生成并存到内存里的

那是由于上述流程图的inMemory…为默认过滤器的验证接口UserDetailsService的实现类,且底层自动写好了

image-20240805112835723

如果我们想要从数据库查询呢?自己重写呗

eg—— xxxx implement UserDetailsService

image-20240805113810677

4-数据库验证案例实战

将快速入门案例拷贝一份作为第二个项目,后续第一个项目有别的操作

4.1-思路分析

登录

  1. 自定义登录接口

默认过滤器链由于校验后返回的a对象由upaf存入security容器中,但是这个类没有存token/权限集合的业务,因此我们要自己写一个自定义接口来生成token并且返回

image-20240805114527268

但是token只包含了userid,如何查询roles权限信息呢?查库?那每次访问鉴权都得查库就太费性能了,第一次登录后生成jwt的同时生成权限集合给他整到redis里,下次直接从redis拿这个权限集合和接口所需的权限集合比对即可
k:userID

v:userinfo(permission…name…password…)

  • 生成jwt
  • 用户信息存redis里
  1. 自定义登录校验类UserDetailsService
  • 查库

校验

①定义jwt过滤器

  • 拿token
  • 解析userid
  • 从redis拿用户信息(拿permission然后再由security校验)

4.2-java准备工作

新建模块-启动类-依赖-hello测试接口,和上面操作一样

工具依赖

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>securityTokenDemo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>Archetype - securityTokenDemo</name>
  <url>http://maven.apache.org</url>
  <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.5.0</version>
  </parent>
  <dependencies>
    <!-- Spring Boot 安全功能的starter包,用于web应用的安全控制 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Spring Boot Web功能的starter包,提供web应用的基本功能 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Lombok,提供简单的代码生成工具,减少样板代码,设置为可选依赖 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!-- Spring Boot的测试starter包,用于单元测试和集成测试 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Spring Security的测试包,用于安全测试 -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Redis的starter包,用于集成Redis作为缓存或持久化方案 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- FastJSON,一个Java语言编写的高性能功能完备的JSON库 -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.33</version>
    </dependency>
    <!-- JWT(JSON Web Token)的库,用于生成和解析JWT -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.0</version>
    </dependency>
    <!-- JAXB API,用于XML和Java对象之间的绑定 -->
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.0</version>
    </dependency>
    <!--mybatis-plus的springboot支持-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.4.3.1</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <!-- Spring Boot的测试starter包,重复项,可能用于不同目的 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
  </dependencies>
</project>

redis配置类

package com.example.securitytest.config;

import com.example.securitytest.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory
                                                               connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}

fastjson工具类

public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Class<T> clazz;

    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t,
                SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return JSON.parseObject(str, clazz);
    }

    protected JavaType getJavaType(Class<?> clazz) {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

jwt工具类

package com.example.securitytest.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {
    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "sangeng";

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,
                                            String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid) //唯一的ID
                .setSubject(subject) // 主题 可以是JSON数据
                .setIssuer("sg") // 签发者
                .setIssuedAt(now) // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg ";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length,
                "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

rediscache工具类

package com.example.securitytest.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

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

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;

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

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

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

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

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

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final
    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(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

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

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final
    T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash =
                redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final
    Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {

        return redisTemplate.keys(pattern);
    }
}

响应渲染类

package com.example.securitytest.utils;

import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String
            string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

User实体类

package com.example.securitytest.domain;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.io.Serializable;

/**
 * <p>
 * 用户表
 * </p>
 *
 * @author 哈纳桑
 * @since 2024-05-07
 */
@TableName("sys_user")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 密码
     */
    private String password;

    /**
     * 用户类型:0代表普通用户,1代表管理员
     */
    private String type;

    /**
     * 账号状态(0正常 1停用)
     */
    private String status;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 手机号
     */
    private String phonenumber;

    /**
     * 用户性别(0男,1女,2未知)
     */
    private String sex;

    /**
     * 头像
     */
    private String avatar;

    /**
     * 创建人的用户id
     */
    private Long createBy;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新人
     */
    private Long updateBy;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 删除标志(0代表未删除,1代表已删除)
     */
    private Integer delFlag;


}

4.3-持久层准备工作

建表

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `type` char(1) DEFAULT '0' COMMENT '用户类型:0代表普通用户,1代表管理员',
  `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
  `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
  `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14787164048663 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

mp依赖(前面引入过了)

        <!-- MyBatis Plus的Spring Boot starter,用于简化MyBatis的使用 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- MySQL连接器,用于连接和操作MySQL数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

yml配置文件

spring:
  application:
    name: SecurityTest
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/sg_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: qq1664546939
  data:
      redis:
        host: localhost
        port: 6379
        password: 123456
        database: 10
server:
  port: 8888

mapper接口

package com.example.securitytest.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.securitytest.domain.User;

public interface UserMapper extends BaseMapper<User> {}

实体类修改

类名指定表,指定主键

@TableName("sys_user")
@TableId(value = "id", type = IdType.*AUTO*)

启动类mapper扫描

@MapperScan("zww.mapper")

单元测试类

新建test/java文件目录,建包zww

image-20240805122852329

package com.zww;


import com.zww.domain.User;
import com.zww.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

/**
 * @BelongsProject: SpringSecurity
 * @BelongsPackage: zww
 * @Author: Zww
 * @CreateTime: 2024-08-05  12:27
 * @Description: TODO
 * @Version: 1.0
 */
@SpringBootTest
public class MapperTest {
    @Resource
    private UserMapper userMapper;

    @Test
    void test() {
        List<User> users =  userMapper.selectList(null);
        System.out.println(users);
    }
}

4.4业务层

实现UserDetails类

既要有userdetails上的玩意还要有自己的user内容那就将user对象作为属性给到userdetials

到时候security就会调用这个对象的getUsername和getPassword所以这里我们给重写的方法返回我们自己的user.getUsername/password

package com.example.domain;

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;

/**
 * @BelongsProject: SpringSecurity
 * @BelongsPackage: com.example.domain
 * @Author: Zww
 * @CreateTime: 2024-08-05  15:39
 * @Description: TODO
 * @Version: 1.0
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private User user;

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

    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}

自定义登录实现类

最后user作为构造函数的参数传入,new出来loginUser也就是userdetails传给接口调用者 Userdetails ud=xxxx.authenticate() 这里不用判断密码是因为authenticate会将查出来LoginUser里的密码与前端传来的进行比对AuthenticationProvider 的 authenticate() 方法确实同时执行了 loadUserByUsername() 和密码比对这两个步骤。

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }
        return new LoginUser(user);

    }
}

4.5测试

由于默认的密码校验需要 前缀有{noop}(noop明文)标识密码类型,否则登录失败

image-20240805155212186

出现用户已锁定,已失效等问题是因为实现userdetails方法里的权限等返回值默认为false,全改为true即可

image-20240805155723283

image-20240805155811547

4.6加密解密

默认使用passwordencoder要求数据库中的密码格式为:{id}xxx以id判断加密方式,一般不采用,这里我们返回一个新的加密对象

继承配置类websecurityconfigurerAdaper

@Configuration //配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

先用明文生成一串加密后的密码,然后登录测试

   @Test
    void createPwd(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123");
        System.out.println(encode);
    }

image-20240805161526545

这串密码给他沾到数据库里去

image-20240805161632811

一样的明文生成不一样的密文,加盐了——————

密码校验

 @Test
    void checkPwd(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        boolean matches = bCryptPasswordEncoder.matches("123", "$2a$10$XPrtEs74Qw.MK9JUuazRj.omKpRpp7Ir9QxrQGE/0ptJH.ZHv.1km");
        System.out.println(matches);
    }

业务代码校验

不new Bcript对象了直接注入解密器

 @Autowired
    private PasswordEncoder passwordEncoder;

@Test
    void checkPwd2(){
        boolean matches=passwordEncoder.matches("123", "$2a$10$XPrtEs74Qw.MK9JUuazRj.omKpRpp7Ir9QxrQGE/0ptJH.ZHv.1km");
        System.out.println(matches);
    }

4.7登录接口

校验成功后我们需要生成一个jwt

jwt工具类及demo

package com.example.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {
    // 有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
    // 设置秘钥明文
    public static final String JWT_KEY = "sangeng";

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,
                                            String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid) // 唯一的ID
                .setSubject(subject) // 主题 可以是JSON数据
                .setIssuer("sg") // 签发者
                .setIssuedAt(now) // 签发时间
                .signWith(signatureAlgorithm, secretKey) // 使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        System.out.println("制作jwt中...");
        String jwt = createJWT("123");
        System.out.println(jwt);
        System.out.println("解析jwt中...");
        String token = jwt;
        Claims claims = parseJWT(token);
        System.out.println(claims.getSubject());
    }

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length,
                "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

运行main方法得到结果

image-20240805163430804

具体思路

通过aManager的authenticate进行用户认证,需要先在security配置类把aManager重写到security配置类,然后加入spring管理
security配置类里加入

 @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

authenticate方法认证成功后 生成jwt给用户一份,给redis一份,id作为key

定义结果类

package com.example.domain;// package com.zww.domain;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

定义service和impl

service

还记得开始那幅图吗,我们前面走了UsernamePasswordAuthenticationFilter过滤器链,现在开始走自定义接口的业务逻辑

image-20240805165628472

用户名和密码需要封装在authent里… 这里使用其实现类upat 作为容器

image-20240805165837202

security配置文件类

加入authenticationManager(前面加过就不用加了)

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

security配置类重写放行login的请求

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                //关闭csrf
                csrf().disable()
                //不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //登录接口放行
                .antMatchers("/user/login").anonymous()
                //其余全部鉴权认证
                .anyRequest().authenticated();
    }

impl

@Service
public class LoginServiceImpl implements LoginService {
    @Resource
    AuthenticationManager authenticationManager;
    @Override
    public ResponseResult login(User user) {
        // 用户名和密码封装到验证容器对象里
        UsernamePasswordAuthenticationToken upat=
                new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        // 开始验证
        Authentication authenticate = authenticationManager.authenticate(upat);
        //如果验证结果也就是返回值 也就是在数据库里查得到,那么就验证通过了
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登陆失败");
        }
        return null;
    }
}

总结

  1. 注入amanager
  2. 调用authenticate方法,传入 实现类upat实现了authentication
  3. 调用自定义userdetailsServiceImpl实现类方法,返回从数据库查询到的Userdetails对象,使用loginUser实现类并传入自己的user类
  4. 接口类判断返回的userdetails是否为空,不为空则拿userid造token,然后将userdetails存redis,后续根据id查询权限
  5. 返回携带token的map给前端

拿upat认证对象调认证方法,根据返回值认证,造token,前端拿一份,redis拿一份认证对象(含roles后续鉴权)

认证,生成令牌,返回前端和redis

4.8jwt验证

过滤器获取token-解析token-拿token查redis-存security容器-放行

jwt过滤器类

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            filterChain.doFilter(request, response);
            // 如果 token 为空,则直接放行请求,让后续的过滤器链继续处理请求。这样做的原因是:
            // 登录接口给security认证
            // 访问控制: 在某些场景下,并不是所有的请求都需要经过身份验证或授权。
            // 比如一些静态资源、登录接口等,可以允许任何用户访问。对于这些不需要验证的请求,
            // 直接放行可以提高系统的灵活性和性能。
            // 过滤器链执行顺序: 过滤器链是一系列过滤器按照特定顺序执行的机制。
            // 如果不在当前过滤器中直接返回,后续的过滤器链会再次进入当前过滤器,造成重复执行。
            // 这可能会导致一些不必要的性能开销或逻辑错误。

			//还有就是。下面逻辑会往security容器存储authentication,后续认证如果检测到了已经存在	
			//authentication,则不会继续认证,即使放行了login接口后续也无法完成校验
            return;
        }

        // 根据token获取userid
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        // 制作redisKey
        String rkey = "login:" + userId;
        // 查询到用户信息
        // 由于getObject封装使用了泛型,不用强转
        LoginUser loginUser = redisCache.getCacheObject(rkey);
        if (Objects.isNull(loginUser)){
            //有token但是查不到,说明token在redis里过期了
            throw new RuntimeException("您的登录状态已过期,请重新登录");
        }
        //存入security容器,以authenticate类型,已认证状态设置为null先
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

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

由于是先进行token校验,然后redis获取loginuser信息,而非直接查询数据库,因该在security链之前,在配置类加入过滤器,并设置在security配置类之前

security的configure应用jwt过滤器

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                // 关闭csrf
                        csrf().disable()
                // 不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 登录接口放行
                .antMatchers("/user/login").anonymous()
                // 其余全部鉴权认证
                .anyRequest().authenticated()
                .and()
                // 异常处理配置
                .exceptionHandling()
                // 认证异常处理
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(403);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(JSON.toJSONString(new ResponseResult(403, "error")));
                })
                // 授权异常处理
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setStatus(403);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(JSON.toJSONString(new ResponseResult(403, "error")));
                });
                //这一步相当于把默认的UsernamePasswordAuthenticationFilter覆盖了因为jwt过滤器链中已经生成了
                 //SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                 //当检测到有authenticationToken则不执行默认的表单认证器UsernamePasswordAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

image-20240805195734140

测试访问

无token

image-20240805204339800

登录后拿token放请求头在请求的结果为

image-20240805205959131

4.9退出接口

so easy, 删redis即可,然后后面每次拿的token userid查token查不到就抛异常了

接口,service,实现类一气呵成

    @Override
    public ResponseResult logout() {
        // jwt过滤器在请求过来的时候给security容器设置了authentication,这里我们直接拿就行
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        String rkey = "login:" + userId;
        redisCache.deleteObject(rkey);
        return new ResponseResult(200, "注销成功");
    }

测试

image-20240805212648528

再来访问其他接口试试看

image-20240805212718445

5-认证配置详解

image-20240805222854105

httpsecurity上的各种接口链式调用

image-20240805223206203

anonymous和permitAll的区别

permitall是所有都能访问,anonymous是未认证可以访问,认证了之后就不能访问

6-授权

6.1实现步骤

security配置类加注解

 @EnableGlobalMethodSecurity(prePostEnabled = true)

接口上加注解

@PreAuthorize("hasAuthority('test')")

自定义实现类添加权限列表

image-20240805231345229

   List<String> permission = new ArrayList<>(Arrays.asList("test","admin"));
        return new LoginUser(user,permission);

loginUser类加权限属性

并完善实现类getAuthorities的逻辑,同时新增一个user,permission的构造函数,删除@AllArgu全参构造注解

image-20240805225008266

image-20240805231241315

package com.example.domain;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @BelongsProject: SpringSecurity
 * @BelongsPackage: com.example.domain
 * @Author: Zww
 * @CreateTime: 2024-08-05  15:39
 * @Description: TODO
 * @Version: 1.0
 */
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private User user;

    private List<String> permission;

    public LoginUser(User user, List<String> permission) {
        this.user = user;
        this.permission = permission;
    }



    //序列化关闭,否则序列化到redis里抛异常,为了安全考虑SimpleGrantedAuthority类型无法存储?
    @JSONField(serialize = false)
    private  List<SimpleGrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 使用GrantedAuthority的实现ctrl+alt+左键查看,
        // 如果每次get鉴权都得处理成集合那就太麻烦了,提升为成员变量永存,每次get进行判断即可
        if (authorities!=null){
            return authorities;
        }
        authorities = permission.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        return null;

    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

JWT过滤器设置权限

image-20240805231816167

最后访问hello接口如果通过即可

6.2数据库查询权限信息

RBAC权限模型

Role Based Access Control角色权限控制模型

用户-角色(权限集合)-权限

用户有什么角色,什么角色对应什么权限,中间建立桥梁表

多对多建立关联表

所有表创建

CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
  `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
  `is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)',
  `menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT '' COMMENT '备注',
  `del_flag` char(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2034 DEFAULT CHARSET=utf8mb3 COMMENT='菜单权限表'

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) NOT NULL COMMENT '角色权限字符串',
  `role_sort` int NOT NULL COMMENT '显示顺序',
  `status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)',
  `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb3 COMMENT='角色信息表'

CREATE TABLE `sys_role_menu` (
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `menu_id` bigint NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='角色和菜单关联表'

CREATE TABLE `sys_user_role` (
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='用户和角色关联表'


内容

image-20240806093350352

SQL语句

  • 根据user role表左连接role表,条件为roleid相等 这样就可以获取用户对应的所有角色

  • 根据用户角色表在左连接menu_role表,条件为roleid相等,这样就可以获取用户的角色包含所有的menuId

  • 根据menuid左连接menu表,条件为桥梁menuId,这样就可以获取用户的所有角色对应的所有菜单权限 perms,

  • select时DISTINCT去重,因为不同角色可能含有相同的菜单权限,比如老板和员工都有资格查看当前菜品的权限

中间桥梁为roleid,将user和menu权限连接

SELECT DISTINCT perms
FROM sys_user_role ur 
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id`=rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id`=rm.`menu_id`
WHERE user_id=1
AND r.`status`=0
AND m.`status`=0

MENU持久层–实体,mapper,mapperxml

实体
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 菜单ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 菜单名称
     */
    private String menuName;

    /**
     * 父菜单ID
     */
    private Long parentId;

    /**
     * 显示顺序
     */
    private Integer orderNum;

    /**
     * 路由地址
     */
    private String path;

    /**
     * 组件路径
     */
    private String component;

    /**
     * 是否为外链(0是 1否)
     */
    private Integer isFrame;

    /**
     * 菜单类型(M目录 C菜单 F按钮)
     */
    private String menuType;

    /**
     * 菜单状态(0显示 1隐藏)
     */
    private String visible;

    /**
     * 菜单状态(0正常 1停用)
     */
    private String status;

    /**
     * 权限标识
     */
    private String perms;

    /**
     * 菜单图标
     */
    private String icon;

    /**
     * 创建者
     */
    private Long createBy;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新者
     */
    private Long updateBy;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 备注
     */
    private String remark;

    private String delFlag;


}

小技巧,装mybatisX插件后对着mapper文件ctrl+回车 可以快速生成xml文件

mapper

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    List<String> selectPermsByUserId(Long userId);
}

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.example.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="com.example.domain.Menu">

        SELECT DISTINCT m.perms
        FROM sys_user_role ur
                 LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
                 LEFT JOIN `sys_role_menu` rm ON ur.`role_id`=rm.`role_id`
                 LEFT JOIN `sys_menu` m ON m.`id`=rm.`menu_id`
        WHERE user_id=#{userId}
          AND r.`status`=0
          AND m.`status`=0
    </select>
</mapper>

yml指定xml路径

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml

测试类测试

image-20240806105747410

修改原来写死的地方

image-20240806110005269

测试从数据库查到的权限能否访问

image-20240806110530646

7-异常信息自定义处理

默认的认证和授权异常为authenticationException和accessdeniedException

作为形参调用各自的接口方法处理异常

认证-authenticationEntryPoint

授权-accessdeniedHandler

而者两个接口的默认实现好像是空吧,因为我们前面没权限他返回值好像啥也没有

我们自己重新建两个实现类实现这两个接口后续将其配置到security配置类后出现异常就会应用我们自己写的实现类里的处理方法

认证

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED, "用户名或密码错误,请重新输入");
        String jsonString = JSON.toJSONString(result);
        // web响应渲染,指定了响应状态码,json格式,编码utf,getWriter写内容
        WebUtils.renderString(response, jsonString);
    }
}

授权

差不多其实,就状态码和msg变了而已

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN, "无权限访问");
        String jsonString = JSON.toJSONString(result);
        // web响应渲染,指定了响应状态码,json格式,编码utf,getWriter写内容
        WebUtils.renderString(response, jsonString);
    }
}

配置到配置类里

注入

image-20240806112104833

image-20240806112329491

   http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

启动测试

测试没权限

image-20240806112456935

测试没认证

image-20240806112513900

8-跨域

浏览器屁事多,有洁癖原则,同源策略, 协议http主机地址www.www.com/192…端口80 要同个格式同个端口,否则报错

但是前后端分离 协议端口主机地址包不一样的

解决访问为,后端处理,允许跨域访问

跨域配置类

@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-20240806113510965

如果要测试可以随便找个前端项目发请求,推荐vue,搞个axios.post一下

用postman测没用的,因为他不是浏览器没有洁癖(同源策略)

9-权限校验

框架提供

源码解析

本质就是将你在接口上定义的权限要求和查询到拥有的权限列表集合作对比

如果你拥有的权限 .contain在需要的权限里,符合要求了,则执行

这里还有一个默认前缀为空的参数,会对其和权限名做一个拼接,下面是图例介绍

首先将注解里的权限要求传入方法…

image-20240806120810891

image-20240806120815381

hasAnyAuthority

传入多个权限要求,只要你有其中一个就可以进入,类似于一个房间,你如果2米9可以进入,你如果喜欢唱歌也可以进入

image-20240806122109189

同时满足就还是用hasAuthority加and and and即可,不过不建议这样,一个菜单对应一个权限有要求就行

hasRole

image-20240806143043004

image-20240806143254475

默认前缀ROLE_,所以我们在数据库里存的权限标识符也得有前缀

hasAnyRole

同hasAnyAuthen一样

自定义权限校验

创建自定义类,写方法,@Component指定名字(“ex”)

@Component("ex")
public class ZwwExpressionRoot {


    public boolean hasAuthority(String authority){
        //获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser principal = (LoginUser) authentication.getPrincipal();
        //前面在自定义认证实现类已经设置了permission
        List<String> permission = principal.getPermission();
       return permission.contains(authority);
    }
}

可以自行断点测试是否走你自定义的那个方法

基于配置类配置权限

image-20240806194444145

其他hasRole都有,调用就完事了

10-CSRF

介绍

你登录了淘宝网站,没退出,保留在淘宝的登录记录(cookie等)

访问了病毒网站,做的和淘宝网站一模一样,点击了按钮,他拿你的身份给淘宝下了单,买了东西,或者转账等敏感操作, 但是这不是你干的,这个行为是他伪装成你做出来的,所以称之为 跨站请求伪造cross-site request forgery

其依靠cookie中携带的认证信息,因为发请求会把cookie一起扔过去,但是我们使用的是在请求头里放token进行认证的呀,这样一来,就不用使用csrf了直接关闭就行

image-20240806200618105

11-自定义认证成功处理器

这个用之前的quickstart项目, 如果还用这个token过滤器的话还得把

和前面的有什么区别吗?

前面的使用了自定义处理类authmanager.authenticate方法 也就是自定义login接口的实现查数据库

这个是表单处理,使用usernamepasswordfilter处理

image-20240806202751615

成功处理类

@Component
public class ZwwAuthenticationSuccess implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功咯");
    }
}

配置类

这是默认的

image-20240806201438969

image-20240806201417543

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    ZwwAuthenticationSuccess zwwAuthenticationSuccess;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用自定义的登录成功处理类
        http.formLogin().successHandler(zwwAuthenticationSuccess);
        //重写了原来默认的配置清空,需要对所有请求继续拦截
        http.authorizeRequests().anyRequest().authenticated();
    }
}

测试

失败处理类

@Component
public class ZwwAuthenticationFail implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("认证失败了");
    }
}

配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    ZwwAuthenticationSuccess zwwAuthenticationSuccess;
    
    @Autowired
    ZwwAuthenticationFail zwwAuthenticationFail;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用自定义的登录成功处理类
        http.formLogin()
                .successHandler(zwwAuthenticationSuccess)
                .failureHandler(zwwAuthenticationFail);
        
        //重写了原来默认的配置清空,需要对所有请求继续拦截
        http.authorizeRequests().anyRequest().authenticated();
    }
}

自行测试

注销成功处理器

重复操作,实现类,注入,configure里添加

自定义类

@Component
public class ZwwLogoutSuccess implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("注销成功");
    }
}

配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    ZwwAuthenticationSuccess zwwAuthenticationSuccess;

    @Autowired
    ZwwAuthenticationFail zwwAuthenticationFail;

    @Autowired
    ZwwLogoutSuccess zwwLogoutSuccess;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用自定义的登录成功处理类
        http.formLogin()
                .successHandler(zwwAuthenticationSuccess)
                .failureHandler(zwwAuthenticationFail);
        http.logout().logoutSuccessHandler(zwwLogoutSuccess);

        //重写了原来默认的配置清空,需要对所有请求继续拦截
        http.authorizeRequests().anyRequest().authenticated();
    }
}

自行测试~

12-其他认证方案畅想

image-20240806204359286

以下为全文回顾,懒得看可跳过

之前方案没用user…filter而是自己写了自定义实现类,调用ProvideManager,authenticationManager实现类上的方法,然后就跑到我们

自己实现且覆盖默认的userdetailsImpl类里面查库,然后查权限,最后返回的值类型为UserDetails实体,我们就用一个LoginUser去实现,然后里面含有我们自己diy的user,同时对其getAuthorities方法重写加工了从数据库查到的权限表,等一些其他类的重写,最后将LoginUser实例形参为user,和权限表permission,然后我们此时还想制作token是吧,用了接口生成了token然后一份放redis一份做了map给前端保存,每次过来请求头携带,然后且进行redis查询,获取到权限列表进行鉴权,就不用老查库获取permission权限集合了,其实我这里有个疑问,如果能存在token里就不用放redis了,但是放redis还有一个好处就是,如果想封禁用户,由于token时间是固定的,我们不能强行不让用户登录,但是可以将redis缓存删掉,登录进来redis查不到就踢出也可以说是拒绝登录也可以说是封禁了,这过程我们还搞了异常处理,开启跨域,还有权限设置,和ROLE BASED ACCESS CONTROL 权限控制模型, 搞了五张表,左连接多表查询最后千辛万苦计算出userid为1的靓仔拥有的权限集合,然后搁接口上进行hasAuthentication判断contains是否匹配,然后放行,难点为jwt过滤器,一开始没token直接放行并且return,这里还有一个是将jwt的过滤器定义在了userfilter之前相当于把demo的那个filter给覆盖了securityContext容器在检测到已经有Authentication已认证过了就不会继续userfilter表单认证…先鉴token,token-》userid查redis然后根据getAuthorities拿到的权限列表后filterChain.doFilter(request, response);,跑到接口那@PreAuthorize(“@ex.hasAuthority(‘system:dept:list’)”)进行下一层权限验证过滤器链校验,最后再执行逻辑,这一段估计有点难记住,先jwt过滤器,然后再权限校验链

其他认证方案

重写userpasswordauthenticationFIlter ,然后还是自定义实现userdetailImpl查库,最后制作token返回前端,不过这一步我们一开始是用login接口实现,还可以把它放在认证成功处理,也就是最后面提到的那个AuthenticationSuccessHandler

如果再验证前加个验证码的校验,就把他写在userpasswordauthenticationFIlter 前。配置类里

13-源码讲解

投票过50…

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值