0


SpringBoot+SpringSecurity+JWT整合

SpringSecurity+JWT整合

1.简介

SpringSecurity是spring的一个安全管理框架,相比另一个安全框架shiro,它提供了更丰富的功能,社区资源也比shiro丰富。

web应用主要用于认证和授权

认证:验证当前访问系统的是不是本系统用户,并且要确定具体是哪个用户

授权:经过认证完成过后判断当前用户是否具有某个权限操作具体资源

springsecurity的核心就是认证和授权,并且更快的整合soring应用

2.快速入门

2.1准备工作

首先搭建一个soringBoot工程

pom依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</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>
    </dependencies>

创建一个测试小例子

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello security";
    }
}

访问http://localhost:8080/hello, 返回字符串hello security

2.2 引入spring security pom依赖

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

再次登录会需要输入用户名和密码,此时默认用户名为: user 密码为控制台输入的密码

Using generated security password: e493f2ae-cb1e-4802-b0a2-1e8da5d97068

3.认证

3.1 登录校验流程

3.2 原理讲解

根据入门案例,分析具体流程

3.2.1 SpringScurity完整流程

springsecurity底层实现是通过一系列的过滤器链完成登录验证和授权等功能,主要是使用到下面15个过滤器

调用流程

认证流程介绍

接口和类介绍:

UsernamePasswordAuthenticationFilter: 认证过滤器主要用于生成Authentication对象完成用户信息封装。

AuthenticationManager: 定义了认证Authentication的方法。

AbstractUserDetailsAuthenticationProvider:提供了authenticate()认证方法,并且在里面调用UserDetailsServiceloadUserByUsername查询用户信息。

UserDetailsService:获取用户信息的核心接口,里面定义了根据用户名查询用户信息,可以实现该接口覆盖默认的用户信息查询方法,完成自定义用户信息查询,返回一个UserDetails用户信息封装对象。

UserDetails: 主要封装用户相关信息,通过UserDetailsService获取用户信息返回UserDetails对象,然后将相关信息设置到Authentication对象中。

3.3 解决问题

3.3.1 思路分析

登录

  1. 自定义登录接口

调用ProviderManager的认证方法,如果认证通过生成jwt并存入到redis

  1. 自定义UserDetailsService

通过自定义的实现完成从数据库查询用户信息和权限信息并返回UserDetails对象

校验

  1. 自定义jwt认证过滤器

获取生成jwt token,解析token获取userid,通过userid从redis中获取用户信息,存入到SecurityContextHolder中,方便后续其他过滤器通过SecurityContextHolder获取用户信息

3.3.2 jwt简介

  1. 什么是 JSON Web Token?

JSON Web 令牌 (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。此信息可以验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

尽管 JWT 可以加密以提供各方之间的保密性,但我们将专注于签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是签名的一方。

跨平台 跨语言 轻量级json 对象进行数据传输, 数字签名保证安全性

应用场景:

  • 授权:这是使用 JWT 的最常见方案。用户登录后,每个后续请求都将包含 JWT,允许用户访问使用该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够跨不同域轻松使用。

  • 信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确定发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改

  1. JSON Web Token令牌结构

JSON Web 令牌由三部分组成,中间用 . 分割

页眉

有效荷载

签名

JWT通常如: xxxxx.yyyyy.zzzzz 组成

  1. 结构详解

页眉 headers 需要Base64进行编码

页面主要包含令牌类型(令牌类型目前只有一种 JWT)和签名算法例如HMAC SHA256 RSA

例如:

{
    "alg" : "SHA256",
    "typ" : "JWT"
}

有效荷载 payload 需要Base64进行编码

荷载主要包含声明,声明分为已注册声明、公共声明、专用声明

已注册声明: 由JWT提前帮我们预定义好的例如iss(发行人),exp(到期时间),sub(主题),aud(受众)等。

公共声明: 这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在 IANA JSON Web 令牌注册表中定义,或者定义为包含抗冲突命名空间的 URI。

专用声明: 这个由用户自定义可以是一个json字符串用户可以将实体信息转换后放入专用声明中,

注意:这些声明中不要包含任何敏感信息

例如

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

签名

要创建签名部分,首先需要Base64编码页眉和有效荷载json数据,然后通过页面中指定的算法以及密钥进行算法加密获取签名的结果。

密钥:是用户自定义的一系列字符串作为私钥,不能提供给其他系统

  1. 签名示例
// 头部信息
{
  "alg": "HS256",
  "typ": "JWT"
}
// 有效荷载
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
// 通过指定算法签名
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)
// 签名后的结果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

3.3.3 java中使用jwt

public class JwtTest {
    // 生成jwt
    @Test
    public void contextLoads() {
        HashMap headers = new HashMap();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, 200);
        String token = JWT.create().withHeader(headers) //headers
                .withClaim("userid", "21")
                .withClaim("username", "wangm") //payload
                .withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256("1231312sdasddsas"));// 密钥
        System.out.println(token);

    }
    // 验证jwt 可以获取页眉数据和有效荷载数据
    @Test
    public void checkToken() {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("1231312sdasddsas")).build();
        DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzI0OTMxMTAsInVzZXJpZCI6IjIxIiwidXNlcm5hbWUiOiJ3YW5nbSJ9.DNmrAFgRnWdcAtBK6nkwNPV-GSsrKyO_TyoIeB0YqzI");
        String userid = decodedJWT.getClaim("userid").asString();
        System.out.println(userid);

    }
}

jwt封装工具类

@Component
public class JwtUtils {
    private static String secretKey;
    @Value("${JWT.secretKey}")
    public void secretKey(String secretKey) {
        JwtUtils.secretKey =  secretKey;
    }
    /*
     * @description: 生成token
     * @author: wangm
     * @date: 2022/12/31 21:47
     * @param: [map, expDay]
     * @return: java.lang.String
     **/
    public static String generateToken(Map<String, String> map, Integer expDay) {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, 1);
        JWTCreator.Builder builder = JWT.create();
        map.forEach((k, v) ->{
            builder.withClaim(k, v);
        });
        builder.withExpiresAt(instance.getTime());
        // 生成token
        return builder.sign(Algorithm.HMAC256(secretKey));
    }
    /*
     * @description: 验证token
     * @author: wangm
     * @date: 2022/12/31 21:47
     * @param: [token]
     * @return: void
     **/
    public static void verifyToken(String token) {
        // 验证token
        JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);

    }
    /*
     * @description: 获取token组成信息
     * @author: wangm
     * @date: 2022/12/31 21:48
     * @param: [token]
     * @return: com.auth0.jwt.interfaces.DecodedJWT
     **/
    public static DecodedJWT getTokenInfo(String token) {
        DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
        return decodedJWT;
    }
}
com.jack.study.security.entity.LoginUser [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[admin]]

jwt整合springboot 整合拦截器统一令牌验证

// 整合拦截器实现统一token 验证
public class JwtInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        Map<String, Object> map = new HashMap<>();
        if (StrUtil.isEmpty(token)) {
            map.put("status", "514");
            map.put("message", "token为空");
            String s = new ObjectMapper().writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(s);
            return false;
        }
        // jwt异常处理
        try{
            JwtUtils.verifyToken(token);
            return true;
        }catch (SignatureVerificationException e1) {
            map.put("status", "510");
            map.put("message", "签名异常");
        }catch (TokenExpiredException e2) {
            map.put("status", "511");
            map.put("message", "token已过期");
        }catch (AlgorithmMismatchException e3) {
            map.put("status", "512");
            map.put("message", "加密算法异常");
        }catch (JWTDecodeException e4) {
            map.put("status", "513");
            map.put("message", "token解密异常");
        }catch (Exception e) {
            map.put("status", "500");
            map.put("message", "系统异常");
        }
        String s = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(s);
        return false;
    }
}

3.3.4 准备工作

  1. 创建一个maven 工程 添加相关依赖

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.jack.study</groupId>
    <artifactId>spring-security-token</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--引入spring security 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--redis 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- jwt 依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <!--swagger 整合bootstarp ui-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
            <exclusions>
                <exclusion>
                    <groupId>io.swagger</groupId>
                    <artifactId>swagger-models</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>

</project>
  1. 创建启动类 并配置相关扫描
@SpringBootApplication(scanBasePackages = {"com.jack.study"}) // 组件扫描路径
@MapperScan(basePackages = {"com.jack.study.*.mapper"}) // mapper文件扫描
public class SecurityTokenApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityTokenApplication.class, args);
    }
}
  1. 定义yml配置
server:
  port: 8080
spring:
#数据库连接配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security-jwt?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    #redis 连接配置
  redis:
    host: localhost
    port: 6379
    password: 123456
mybatis-plus:
#mapper xml扫描配置
  mapper-locations: classpath:/mapper/*.xml
  #sql执行日志输出配置
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#jwt 自定义配置
Jwt:
    #jwt签名私钥
  secretKey: wwwwww
  #jwt 令牌有效期 单位毫秒
  ttlMillis: 360000
  1. 项目相关工具类和统一封装

响应统一封装实体类

//响应代码枚举
public enum ResultCode {
    /***
     *
     * @description:
     * # 200         表示请求成功
     * #1000~1999 区间表示参数错误
     * #2000~2999 区间表示用户错误
     * #3000~3999 区间表示接口异常
     * #4000~4999 区间表示网络请求错误
     * #5000~5999 区间表示服务器内部错误
     * @Date: 2020/11/4 13:25
     **/
    //请求成功
    SUCCESS(200,"成功"),
    PARAM_IS_INVALID(1001,"参数为空"),
    OK(200, "操作成功"),
    FAIL(500, "操作失败"),
    NO_AUTH(403, "没有权限"),
    NO_PAGE(404, "未找到页面"),
    TOKEN_OVERDUE(400, "Token超期");
    //参数校检

    private Integer code;
    private String message;
    ResultCode(Integer code,String message){
        this.code = code;
        this.message = message;
    }
    public Integer code(){
        return this.code;
    }
    public String message(){
        return this.message;
    }

}

// 响应结果实体
@Data
public class Result implements Serializable {
    /**
     * 返回码
     */
    private Integer code;
    /**
     *  返回消息
     */
    private String message;
    /**
     * 返回数据
     */
    private Object data;
    public Result(){

    }
    //自定义错误代码和错误信息
    public Result(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    //根据枚举固定错误代码获取
    public Result(ResultCode resultCode, Object data){
        this.code = resultCode.code();
        this.message = resultCode.message();
        this.data = data;

    }
    /***
     *
     * @description: 操作成功不返回数据
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result success(){
        Result result = new Result();
        result.setCode(ResultCode.SUCCESS.code());
        result.setMessage(ResultCode.SUCCESS.message());
        return result;
    }
    /***
     *
     * @description: 操作成功 返回请求数据
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result success(Object data){
        Result result = new Result();
        result.setCode(ResultCode.SUCCESS.code());
        result.setMessage(ResultCode.SUCCESS.message());
        result.setData(data);
        return result;

    }
    /***
     *
     * @description: 操作失败 返回请求数据
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result failure(ResultCode resultCode){
        Result result = new Result();
        result.setCode(resultCode.code());
        result.setMessage(resultCode.message());
        return result;

    }
    /***
     *
     * @description: 操作失败 返回请求数据
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result failure(Integer code, String message){
        Result result = new Result();
        result.setCode(code);
        result.setMessage(message);
        return result;

    }
    /***
     *
     * @description: 操作成功 返回请求数据
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result failure(ResultCode resultCode,Object data){
        Result result = new Result();
        result.setCode(resultCode.code());
        result.setMessage(resultCode.message());
        result.setData(data);
        return result;

    }
    /***
     *
     * @description: 自定义返回码和返回信息
     * @Date: 2020/11/4 16:36
     * @return: com.currency.server.entity.response.Result
     **/
    public static Result custom(Integer code, String message, Object data){
        Result result = new Result();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;

    }
}

异常封装统一处理

认证异常工具类

public class AuthExceptionUtil {
    public static Result getErrMsgByExceptionType(AuthenticationException e) {
        if (e instanceof LockedException) {
            return Result.failure(1100, "账户被锁定,请联系管理员!");
        } else if (e instanceof CredentialsExpiredException) {
            return Result.failure(1105,"用户名或者密码输入错误!");
        }else if (e instanceof InsufficientAuthenticationException) {
            return Result.failure(403,"权限不足请登录!");
        } else if (e instanceof AccountExpiredException) {
            return Result.failure(1101, "账户过期,请联系管理员!");
        } else if (e instanceof DisabledException) {
            return Result.failure(1102, ("账户被禁用,请联系管理员!"));
        } else if (e instanceof BadCredentialsException) {
            return Result.failure(1105, "用户名或者密码输入错误!");
        }else if (e instanceof AuthenticationServiceException) {
            return Result.failure(1106, "认证失败,请重试!");
        }

        return Result.failure(1200, e.getMessage());
    }
    public static Result getErrMsgByExceptionType(AccessDeniedException e) {
        if (e instanceof CsrfException) {
            return Result.failure(-1001, "非法访问跨域请求异常!");
        } else if (e instanceof org.springframework.security.web.csrf.CsrfException) {
            return Result.failure(-1002,"非法访问跨域请求异常!");
        } else if (e instanceof AuthorizationServiceException) {
            return Result.failure(1101, "认证服务异常请重试!");
        }else if (e instanceof AccessDeniedException) {
            return Result.failure(4003, "权限不足不允许访问!");
        }

        return Result.failure(1200, e.getMessage());
    }
}

认证业务异常封装

public class BusinessException extends AuthenticationException {
    public BusinessException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public BusinessException(String msg) {
        super(msg);
    }
}

统一异常处理,根据自定义异常分别处理错误信息

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 根据实际自定义的异常统一处理
    @ExceptionHandler(value = BusinessException.class)
    public Result operationError(BusinessException e) {
        return Result.failure(ResultCode.FAIL, e.getMessage());
    }

}

redisConfig 配置类

@Configuration
public class RedisConfig {

    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;
    }
}

fastjoson 序列化

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;
    }

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

    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);
    }
}

redis 工具类封装

@Component
@Order(-1)
public final class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return 0
     */

    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {

        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {

        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {

        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */

    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */

    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */

    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * 0
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {

        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {

        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {

        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     * @return 0
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
     * @return 0
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return 0
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return 0
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

webUtil response 响应封装

public class WebUtils {
    public static String rednerString(HttpServletResponse response, String content) {
        try{
            response.setStatus(200);
            response.setContentType("application/json;charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().print(content);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

3.3.5 实现

3.3.5.1 准备数据库用户

从前面的分析可以知道我们自定义个UserDetailsService 实现类即可完成从数据库通过用户名和密码完成认证

创建用户表

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
  `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
  `status` varchar(50) NOT NULL DEFAULT '0' COMMENT '账号状态(0 正常 1 停用)',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
  `phonenumber` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '联系方式',
  `sex` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户性别(0 男 1 女 2未知)',
  `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像',
  `user_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户类型(0 管理员 1 普通用户)',
  `create_by` bigint DEFAULT NULL COMMENT '创建人id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新人id',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `del_flag` int NOT NULL DEFAULT '0' COMMENT '删除标志(0 未删除  1 已删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
3.3.5.2 创建操作用户相关接口和类

用户实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class UserDO implements Serializable {
    private static final long serialVersionUID = -7637085672143873931L;
    /*
     * 主键id
    **/
    private Long id;
    /*
     * 用户名
    **/
    private String username;
    /*
     * 密码
    **/
    private String password;
    /*
     * 昵称
    **/
    private String nickname;
    /*
     * 账号状态(0 正常 1 停用)
    **/
    private String status;
    /*
     * 邮箱
    **/
    private String email;
    /*
     * 联系方式
    **/
    private String phonenumber;
    /*
     * 用户性别(0 男 1 女 2未知)
    **/
    private String sex;
    /*
     * 头像
    **/
    private String avatar;
    /*
     * 用户类型(0 管理员 1 普通用户)
    **/
    private String userType;
    /*
     * 创建人id
    **/
    private Long createBy;
    /*
     * 创建时间
    **/
    private LocalDateTime createTime;
    /*
     * 更新人id
    **/
    private Long updateBy;
    /*
     * 更新时间
    **/
    private LocalDateTime updateTime;
    /*
     * 删除标志(0 未删除  1 已删除)
    **/
    private Integer delFlag;

}

定义UserMapper和相关方法

public interface UserMapper extends BaseMapper<UserDO> {
}

定义UserService和相关方法

public interface UserService extends IService<UserDO> {
    UserDO getUserByUsername(String username);
}

定义UserServiceImpl和相关方法

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDO getUserByUsername(String username) {
        UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUsername, username));
        if (Objects.isNull(userDO)) {
            throw new BusinessException("用户信息不存在!");
        }
        return userDO;
    }
}
3.3.5.3 创建操作角色接口和相关类

创建角色表

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `name` varchar(128) NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) NOT NULL COMMENT '角色代码',
  `status` char(1) NOT NULL DEFAULT '0' COMMENT '角色状态(0 正常 1 停用)',
  `del_flag` int NOT NULL DEFAULT '0' COMMENT '删除标志(0 正常 -1 已删除)',
  `create_by` bigint DEFAULT NULL COMMENT '创建人',
  `update_by` bigint DEFAULT NULL COMMENT '更新人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';

创建SysRole实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysRole {
    /*
     * 主键id
    **/
    private Long id;
    /*
     * 角色名称
    **/
    private String name;
    /*
     * 角色代码
    **/
    private String roleKey;
    /*
     * 角色状态(0 正常 1 停用)
    **/
    private String status;
    /*
     * 删除标志(0 正常 -1 已删除)
    **/
    private Integer delFlag;
    /*
     * 创建人
    **/
    private Long createBy;
    /*
     * 更新人
    **/
    private Long updateBy;
    /*
     * 创建时间
    **/
    private LocalDateTime createTime;
    /*
     * 更新时间
    **/
    private LocalDateTime updateTime;
    /*
     * 备注
    **/
    private String remark;
}

创建SysRoleMapper接口

public interface SysRoleMapper extends BaseMapper<SysRole> {
    List<String> selectRolesByUserId(@Param("userId") Long userId);
}

创建SysRoleService 接口

public interface SysRoleService extends IService<SysRole> {
    List<String> selectRolesByUserId(@Param("userId") Long userId);

}

创建SysRoleService 接口实现类SysRoleServiceImpl

@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Override
    public List<String> selectRolesByUserId(Long userId) {
        return sysRoleMapper.selectRolesByUserId(userId);
    }
}

创建SysRoleMapper.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.jack.study.role.mapper.SysRoleMapper">
    <select id="selectRolesByUserId" resultType="java.lang.String">
        select sr.role_key   from sys_role sr
            left join sys_user_role sur on sur.role_id = sr.id
            <where>
                sr.status ='0'
                <if test="userId != null">
                    and sur.user_id=#{userId}
                </if>
            </where>
    </select>
</mapper>
3.3.5.4 创建操作菜单接口和相关类

创建角色表

CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `menu_name` varchar(64) NOT NULL COMMENT '菜单名称',
  `path` varchar(200) NOT NULL COMMENT '菜单路径',
  `component` varchar(255) NOT NULL COMMENT '组件',
  `visible` char(1) NOT NULL DEFAULT '0' COMMENT '菜单显示(0 显示 1 隐藏)',
  `status` char(1) NOT NULL DEFAULT '0' COMMENT '菜单状态(0 启用 1 停用)',
  `perms` varchar(100) NOT NULL COMMENT '权限集合标识',
  `icon` varchar(100) DEFAULT NULL COMMENT '图标',
  `create_by` bigint DEFAULT NULL COMMENT '创建人',
  `update_by` bigint DEFAULT NULL COMMENT '更新人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` int NOT NULL DEFAULT '0' COMMENT '删除标志(0 正常 -1 已删除)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单表';

创建SysMenu实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysMenu {
    private Long id;
    private String menuName;
    private String path;
    private String component;
    private String visible;
    private String status;
    private String perms;
    private String icon;
    private Long createBy;
    private Long updateBy;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Integer delFlag;
    private String remark;
}

创建SysMenuMapper接口

public interface SysMenuMapper extends BaseMapper<SysMenu> {
    List<String> selectPermsByUserId(@Param("userId") Long userId);
}

创建SysMenuService接口

public interface SysMenuService extends IService<SysMenu> {
    List<String> selectPermsByUserId(Long userId);
}

创建SysMenuService接口实现类SysMenuServiceImpl

@Service
public class SysMenuServiceImpl  extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
    @Autowired
    private SysMenuMapper sysMenuMapper;
    @Override
    public List<String> selectPermsByUserId(Long userId) {
        return sysMenuMapper.selectPermsByUserId(userId);
    }
}

创建SysMenuMapper.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.jack.study.menu.mapper.SysMenuMapper">
    
    
<select id="selectPermsByUserId" resultType="java.lang.String">
    select sm.perms from sys_user_role sur
    left join sys_role sr on sr.id = sur.role_id
    left join sys_role_menu srm on srm.role_id = sr.id
    left join sys_menu  sm on sm.id = srm.menu_id
    <where>
        sr.`status`='0'
        and sm.status = 0
        <if test="userId != null ">
            and sur.user_id=#{userId}
        </if>
    </where>
    group by sm.perms

</select>

</mapper>
3.3.5.5 创建用户角色关联

用户角色sql

CREATE TABLE `sys_user_role` (
  `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `role_id` bigint NOT NULL COMMENT '角色id',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';

用户角色实体

@Data
public class SysUserRole {
    /*
     * 用户id
    **/
    private Long userId;
    /*
     * 角色id
    **/
    private Long roleId;
}
3.3.5.6创建 角色菜单关联

角色菜单sql

CREATE TABLE `sys_role_menu` (
  `role_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'roleId',
  `menu_id` bigint NOT NULL COMMENT 'menuId',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='sys_role_menu';
3.3.5.67登录登出接口

登录登出LoginService

public interface LoginService {
    Result login(UserDto userDto);

    Result logout();
}
@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Override
    public Result login(UserDto userDto) {
        // 1 获取AuthenticationManager 对象 然后调用 authenticate() 方法
        // UsernamePasswordAuthenticationToken 实现了Authentication 接口
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //2 认证没通过 提示认证失败
        if (Objects.isNull(authenticate)) {
            throw new BusinessException("认证失败用户信息不存在");
        }
        //认证通过 使用userid 生成jwt token令牌
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String s = loginUser.getUserDO().getId().toString();
        String token = JwtUtils.createJwt(s);
        // 把用户信息存入到redis中
        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        redisUtil.set("login:"+s, loginUser);
        return Result.success(map);
    }

    @Override
    public Result logout() {
        // 1 获取 SecurityContextHolder 中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        //2 删除redis 中的缓存信
        String key = "login:"+loginUser.getUserDO().getId().toString();
        redisUtil.del(key);
        return Result.success("退出成功!");

    }
}

3.3.6 SpringSecurity 配置重写

3.3.6.1 自定义UserDetailsService实现MyUserDetailsService

具体配置需要将其他组件写好再配置

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       
        return null;
    }
}
3.3.6.2 自定义WebSecurityConfigurerAdapter 实现类SecurityConfig

具体配置需要将其他组件写好再配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
3.3.6.3 定义认证和授权相关组件

再认证过程中通过自定义的UserDetailsService,重写了loadUserByUsername 方法返回UserDetails对象,我们可以通过实现UserDetails 完成自定义用户信息返回

自定义LoginUser对象

@Data
@NoArgsConstructor
public class LoginUser  implements UserDetails{
    // 用户信息
    private UserDO userDO;
    // 权限信息
    private List<String> permissions;
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;
    public LoginUser(UserDO userDO, List<String> permissions){
        this.userDO = userDO;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 将权限信息封装成 SimpleGrantedAuthority
        if (authorities != null) {
            return authorities;
        }
         authorities = this.permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return this.userDO.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;
    }
}

自定义MyUserDetailsService,实现自定义的用户名密码校验

@Service
public class MyUserDetailsService implements UserDetailsService {
// 用户信息
    @Autowired
    private UserService userService;
    //权限菜单信息
    @Autowired
    private SysMenuService sysMenuService;
    //角色信息
    @Autowired
    private SysRoleService sysRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDO userDO = userService.getUserByUsername(username);
        // 获取用户所有的角色
        List<String> roles = sysRoleService.selectRolesByUserId(userDO.getId());
        Set<String> set = roles.stream().map(s -> "ROLE_" + s).collect(Collectors.toSet());
        // 获取用户所有的权限
        List<String> permissions = sysMenuService.selectPermsByUserId(userDO.getId());
        permissions.addAll(set);
        return new LoginUser(userDO, permissions);
    }
}
3.3.6.4 自定义认证异常和授权异常处理

自定义认证失败处理AuthenticationEntryPointImpl

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        WebUtils.rednerString(httpServletResponse, JSONUtil.toJsonStr(AuthExceptionUtil.getErrMsgByExceptionType(e)));
    }
}

自定义授权失败处理AccessDeniedHandlerImpl

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        WebUtils.rednerString(httpServletResponse, JSONUtil.toJsonStr(AuthExceptionUtil.getErrMsgByExceptionType(e)));

    }
}
3.3.6.5 整合jwt,自定义JWT认证过滤器

自定义jwt 认证token过滤器JwtAuthenticationFilter

@Component
public class JwtAuthenticationFilter  extends OncePerRequestFilter {
    @Autowired
    private RedisUtil redisUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws BusinessException, ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (StrUtil.isEmpty(token)) {
            // token不存在 放行 并且直接return 返回
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId = null;
        try {
            Claims claims = JwtUtils.parseJwt(token);
             userId = claims.getSubject();
        } catch (Exception e) {
            throw new BusinessException("token非法");
        }
        // 获取userid 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = (LoginUser)redisUtil.get(redisKey);
        if (Objects.isNull(loginUser)) {
            throw new BusinessException("用户未登录");

        }

        //将用户信息存入到SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}
3.3.6.6 跨域请求配置

自定义CorsConfig配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .maxAge(3600);

    }
    @Bean
    public CorsConfiguration corsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader("token");
        return corsConfiguration;
    }
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",  corsConfiguration());
        return new CorsFilter(source);
    }
}
3.3.6.7 整合到SpringSecurity

SpringSecurity核心配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    // 自定义密码加密配置常用的BCryptPasswordEncoder,也可以用其他的
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭csrf
                .csrf().disable()
                // 不通过session 获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
                // 允许登录接口匿名访问
                .antMatchers("/user/login", "/user/test").anonymous()
                // 其他请求都需要认证
               .anyRequest().authenticated();
               // 在UsernamePasswordAuthenticationFilter 过滤器之前执行jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        // 认证授权异常自定义处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
        // 跨域请求配置        
        http.cors();
    }
}
3.3.6.8 测试

编写LoginController

@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;
    // 登录接口 需要传入用户名和密码
    @PostMapping("/user/login")
    public Result login(@RequestBody UserDto userDto) {
        Result result = loginService.login(userDto);
        return result;
    }

    @PostMapping("/user/hello")
    public Result login() {

        return Result.success();
    }
    //等处配置
    @GetMapping("/user/logout")
    public Result logout() {
        return loginService.logout();
    }
}

使用postman工具进行测试

登录接口测试

退出接口测试,需要在请求头中携带token

鉴权测试

当前用户权限为编码

select sm.perms from sys_user_role sur
    left join sys_role sr on sr.id = sur.role_id
    left join sys_role_menu srm on srm.role_id = sr.id
    left join sys_menu  sm on sm.id = srm.menu_id
    where
        sr.`status`='0'
        and sm.status = 0
        
            and sur.user_id=1
       
    group by sm.perms

@RestController
public class HelloController {
    @GetMapping("/hello/test")
    // 只有system:dept:list:test 权限才能访问
    @PreAuthorize("hasAuthority('system:dept:list:test')")
    public String hello() {
        return "hello";
    }

}

请求头中携带登录token,测试结果

正常访问

@RestController
public class HelloController {
    @GetMapping("/hello/test")
    // 只有system:dept:list:test 权限才能访问
    @PreAuthorize("hasAuthority('system:dept:list')")
    public String hello() {
        return "hello";
    }

}

返回 字符串 "hello"

其他鉴权测试同上,下面讲解其他鉴权的含义

@RestController
public class HelloController {
    @GetMapping("/hello/test")
    //必须包含system:dept:list 权限才能访问否则提示没有权限
    //@PreAuthorize("hasAuthority('system:dept:list')")
    // 只需要有其中一个权限就能访问
    //@PreAuthorize("hasAnyAuthority('system:dept:list555','system:dept:list')")
    //只需要包含其中一个角色就能访问
    //@PreAuthorize("hasAnyRole('ceo', 'admin')")
    //必须包含指定角色才能访问
    //@PreAuthorize("hasRole('ceo')")
    // 自定义权限判断
    @PreAuthorize("@ex.hasAuthority('ROLE_ceo')")
    public String hello() {
        return "hello";
    }

}
3.3.6.9 自定义权限判断

源码探究

不管是hasAuthority 、hasAnyAuthority、 hasAnyRole、 hasRole都是在SecurityExpressionRoot类中实现

SecurityExpressionRoot

public final boolean hasAuthority(String authority) {
        return this.hasAnyAuthority(authority);
    }

    public final boolean hasAnyAuthority(String... authorities) {
        return this.hasAnyAuthorityName((String)null, authorities);
    }

    public final boolean hasRole(String role) {
        return this.hasAnyRole(role);
    }

    public final boolean hasAnyRole(String... roles) {
        return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
    }

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        //获取当前用户的所有权限和角色
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            // 如果当前用户里面包含注解里面的角色或者权限则返回true 允许访问
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        //返回false 不允许访问
        return false;
    }

this.getAuthoritySet() 源码探究

 private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            // 这里是从authentication 中获权限,在前面认证授权过程中我们将从数据查询道德角色权限信息通过自定义的LoginUser 设置到authentication 中,此时用户就能拿到对应的角色权限信息
            
             //JwtAuthenticationFilter 过滤器中将用户信息存入到SecurityContextHolder
       // UsernamePasswordAuthenticationToken authenticationToken = new //UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
   //     SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            //转换为set集合
            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }

        return this.roles;
    }

通过上面的源码我们知道 只需要在鉴权的时候返回true或false 就能实现鉴权访问

自定义JackExpressionRoot

@Component("ex")
public class JackExpressionRoot {
    public final boolean hasAuthority(String authority) {
        LoginUser loginUser = (LoginUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        return permissions.contains(authority);
    }
}

我们只需要按照security 鉴权判断方式编写即可实现自定义鉴权

    //实现@加组件名称.方法名('权限编码或ROLE_角色编码')    
@PreAuthorize("@ex.hasAuthority('ROLE_ceo')")
    public String hello() {
        return "hello";
    }

3.3.7 扩展说明

3.3.7.1 自定义认证 授权 登出

在SecurityConfig 中我们也可以去自定义认证成功或失败,以及登出成功实现自定义处理

自定义认证成功处理MySuccessHandler

@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功调用");
    }
}

自定义认证失败处理

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

自定义登出成功处理MyLogoutSuccessHandler

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

将自定义的组件配置到SecurityConfig中

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MySuccessHandler successHandler;
    @Autowired
    private MyFailureHandler failureHandler;
    @Autowired
    private MyLogoutSuccessHandler logoutSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                // 认证成功 调用
                .successHandler(successHandler)
                // 认证失败调用
                .failureHandler(failureHandler)
                .and()
                // 登出成功调用
                .logout().logoutSuccessHandler(logoutSuccessHandler)
                .and().authorizeRequests().anyRequest().authenticated();
    }
}
3.3.7.2 增加验证码

验证码应该时在登录之前进行验证,因此可以通过过滤器实现

@Component
public class CaptchaFilter  extends OncePerRequestFilter {
    //保存验证码的方式 redis  session
    @Value("${application.captcha.type}")
    private String saveType;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private LoginFailureHandler failureHandler;
    @Autowired
    private LoginSuccessHandler successHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String url = request.getRequestURI();
        if ("/login".equals(url) && "POST".equalsIgnoreCase(request.getMethod())) {
            //校验验证码
            try{
                validate(request);
            }catch (CaptchaException e) {
                //失败单独处理交给认证失败处理器
                failureHandler.onAuthenticationFailure(request, response, e);
            }
        }
        // 验证通过放行
        filterChain.doFilter(request, response);
    }
    public void validate (HttpServletRequest request) {
        //form提交的code
        String uuid = request.getParameter("uuid");
        String code = request.getParameter("code");
        String key = "";
        if (StrUtil.isEmpty(code) || StrUtil.isEmpty(uuid)) {
            throw new CaptchaException("验证码错误");
        }
        if("SESSION".equals(saveType)){
            key = CommonConstant.API_USER_IMAGE_CODE_KEY+saveType+uuid;
            Object redisCode = request.getSession().getAttribute(key);
            if (StrUtil.isBlankIfStr(redisCode) || !(redisCode).equals(code)) {
                throw new CaptchaException("验证码错误");
            }
            // 一次性使用验证完成删除
            request.getSession().removeAttribute(key);
        }else{
            key = CommonConstant.API_USER_IMAGE_CODE_KEY+saveType+uuid;
            Object redisCode = redisUtil.get(key);
            if (StrUtil.isBlankIfStr(redisCode) || !(redisCode).equals(code)) {
                throw new CaptchaException("验证码错误");
            }
            // 一次性使用验证完成删除
            redisUtil.del(key);
        }

    }
}

也可以通过继承BasicAuthenticationFilter 实现jwt 认证过滤器

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserDetailServiceImp userDetailsService;
    @Autowired
    private ISysUserService sysUserService;
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader(jwtUtil.getHeader());
        if (StrUtil.isBlankOrUndefined(header)) {
            chain.doFilter(request, response);
            return;
        }
        Claims claims = jwtUtil.getClaimsByToken(header);
        if (claims == null) {
            throw new JwtException("token异常");
        }
        if (jwtUtil.isTokenExpired(claims)) {
            throw new JwtException("token已过期");
        }
        // 获取用户名
        String username = claims.getSubject();
        //获取用户权限信息
        SysUser sysUser = sysUserService.getByUsername(username);
        if (sysUser != null) {
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailsService.getUserGrantedAuthority(sysUser.getId(), username));
            SecurityContextHolder.getContext().setAuthentication(token);
        } else {
            // 将token存入Authentication中
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, null);
            SecurityContextHolder.getContext().setAuthentication中(token);
        }
        chain.doFilter(request, response);
    }
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MySuccessHandler successHandler;
    @Autowired
    private MyFailureHandler failureHandler;
    @Autowired
    private MyLogoutSuccessHandler logoutSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                // 认证成功 调用
                .successHandler(successHandler)
                // 认证失败调用
                .failureHandler(failureHandler)
                .and()
                // 登出成功调用
                .logout().logoutSuccessHandler(logoutSuccessHandler)
                .and().authorizeRequests().anyRequest().authenticated()
                 .addFilter(jwtAuthenticationFilter())
                 // 在UsernamePasswordAuthenticationFilter 先执行验证码过滤器
                .addFilterBefore(captchaFilter,                 UsernamePasswordAuthenticationFilter.class);

    }
}
标签: java spring boot

本文转载自: https://blog.csdn.net/seeyouagain_/article/details/128549455
版权归原作者 seeyouagain_ 所有, 如有侵权,请联系我们删除。

“SpringBoot+SpringSecurity+JWT整合”的评论:

还没有评论