0
点赞
收藏
分享

微信扫一扫

Spring Security详细讲解(JWT+SpringSecurity登入案例)

本篇博文目录:

一.SpringSecurity简介

1.SpringSecurity

官网地址:
Spring Security简介https://spring.io/projects/spring-security

在这里插入图片描述
Spring Security英文教程:https://docs.spring.io/spring-security/reference/index.html
在这里插入图片描述
Spring Security中文教程:https://docs.gitcode.net/spring/guide/spring-security/overview.html
在这里插入图片描述

2.SpringSecurity相关概念

  • 什么是SpringSecurity
  • 关于什么是认证和授权
  • SpringSecurity 特点
  • SpringSecurity和Shiro的比较

二.认证和授权

1.认证

(1) 使用SpringSecurity进行简单的认证(SpringBoot项目中)

创建一个SpringBoot的Web项目,功能非常简单就是通过访问sayHello接口,在游览器输出字符串 "Hello,SpringSecurity" :
在这里插入图片描述
项目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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dudu</groupId>
    <artifactId>springsecuritydemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecuritydemo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--springBootWeb依赖-->
        <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>

        <!--test测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

controller下创建SpringSecurityController:
在这里插入图片描述

运行项目,访问sayHello接口,搞定:
在这里插入图片描述

接下来在项目的pom.xml配置文件中导入SpringSecurity的依赖:

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

再次访问sayHello接口,就会跳转到一个登入界面,在项目中我们并没有编写该登入界面的代码,其实这就是SpringSecurity 内置的用户身份验证:
在这里插入图片描述
身份验证默认的账号为:user,密码在项目启动的时候在控制台会打印,注意每次启动的时候密码会发生变化!
在这里插入图片描述

输入账号和密码后点击登入:
在这里插入图片描述
登入后,就能够成功访问sayHello接口了:
在这里插入图片描述

(2) SpringSecurity的原理

在这里插入图片描述
上图中的三个过滤器分别是:

在项目启动类中,按下图所示进行debug,就可以观察到这个过滤链,过滤链上一共有16个过滤器( run.getBean(DefaultSecurityFilterChain.class) )
在这里插入图片描述
这16个过滤器分别是:
在这里插入图片描述

(3) SpringSecurity核心类

  • Authentication
  • SecurityContextHolder
  • AuthenticationManager
  Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • AuthenticationProvider
  • UserDetailsService
  • UserDetails

常用方法:

方法名解释
Collection<?extendsGrantedAuthority> getAuthorities();表示获取登录用户所有权限
String getPassword();表示获取密码
String getUsername();表示获取用户名
boolean isAccountNonExpired();表示判断账户是否过期
boolean isAccountNonLocked();表示判断账户是否被锁定
boolean isCredentialsNonExpired();表示凭证{密码}是否过期
boolean isEnabled();表示当前用户是否可用
  • PasswordEncoder

常用方法:

方法名解释
String encode(CharSequence rawPassword);表示把参数按照特定的解析规则进行解析
boolean matches(CharSequence rawPassword, String encodedPassword);表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
default boolean upgradeEncoding(String encodedPassword)表示如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则返回false。默认返回false。

内置的PasswordEncoder实现列表:

实现类说明
NoOpPasswordEncoder(已废除)明文密码加密方式,该方式已被废除(不建议在生产环境使用),不过还是支持开发阶段测试Spring Security的时候使用。
BCryptPasswordEncoder使用广泛支持的bcrypt 算法来散列密码
Argon2Passwordencoder使用Argon2 算法来散列密码, 是一种故意缓慢的算法,需要大量内存
PBKDF2PASSWORDENCODER使用PBKDF2 算法来散列密码,是一种故意缓慢的算法
SCryptPasswordEncoder使用scrypt算法来散列密码,是一种故意缓慢的算法,需要大量内存
  1. 有许多应用程序使用旧的密码编码,无法轻松地进行迁移。
  2. 密码存储的最佳实践将再次改变。
  3. 作为一种框架 Spring,安全不能频繁地进行破坏更改
  1. 确保使用当前的密码存储建议对密码进行编码
  2. 允许验证现代和遗留格式的密码。
  3. 允许在将来升级编码

方式1:创建默认的代理 PasswordEncoder(SpringSecurity5.x)

在这里插入图片描述
我们来看看createDelegatingPasswordEncoder()的源码:

在这里插入图片描述

运行效果:

在这里插入图片描述

方式2:创建自定义代理 PasswordEncoder

在这里插入图片描述
运行效果:
在这里插入图片描述

实际项目中如果不采用默认方式,可以通过@Bean的方式来统一配置全局共用的PasswordEncoder,如下所示:
在这里插入图片描述

上图所示在配置中还重新配置了users
在这里插入图片描述

就一个用户,用户名为user,密码123456,使用该用户进行登入:
在这里插入图片描述
登入成功:
在这里插入图片描述

(4) 认证登入案例(JWT+SpringSecurity实现登入案例)

SpringSecurity的认证流程图如下,如果不按默认方式进行,将对应接口的实现类换成自己的实现类即可:
在这里插入图片描述
JWT的流程图如下:
在这里插入图片描述

JWT+SpringSecurity实现登入案例:

在这里插入图片描述

项目中有一个SecurityConfig类,表示SpringSecurity的配置
在这里插入图片描述
这个configure配置是整个Security Config的配置(过滤器链的配置),下图红框中的配置表示在UsernamePasswordAuthenticationFilter过滤器前添加一个过滤器。
在这里插入图片描述
这个过滤器是继承OncePerRequestFilter过滤器(之所以继承 OncePerRequestFilter 因为OncePerRequestFilter 一个请求只被过滤器拦截一次。请求转发不会第二次触发过滤器 ,而Filter会触发二次)
在这里插入图片描述
该过滤器存放在component文件下的JwtAuthenticationTokenFilter类
在这里插入图片描述

程序运行后,过滤器会一个一个的执行当执行到UsernamePasswordAuthenticationFilter过滤器之前会先执行自定义的JwtAuthenticationTokenFilter过滤器中的doFilterInternal()方法:
在这里插入图片描述
该方法业务逻辑非常清晰,就是判断是否在请求头中存在JWT的Token,如果存在( 用户已经登入过了 )就从token的载荷中获取用户名,然后再根据UserDetaulsService的loadUserByUsername(username)获取用户信息 (UserDetaulsService被替换成从数据库中获取用户信息,详细代码如下图所示 ) 然后进行认证,如果不存在( 该请求为登入请求 ),用户的信息从登入界面获取( 用户名,密码 )然后进行认证。
 图1
这里的AdminUserDetails其实就是UserDetails接口的实现类(进行了扩展):
在这里插入图片描述

自定义的JwtAuthenticationTokenFilter过滤器放行后,就会执行UsernamePasswordAuthenticationFilter过滤器,执行过程如下图:
在这里插入图片描述
下面通过Postman进行模拟
用户首次登入,访问http://localhost:8089/admin/login接口,并携带账号和密码:
在这里插入图片描述
在自定义JwtAuthenticationTokenFilter过滤器的doFilterInternal()方法中打断点,进行观察,因为首次登入头部没有信息,所以authHeader=null,最后 chain.doFilter(request, response);进行放行
在这里插入图片描述
接下来进行认证,代码会执行业务层的login(String username, String password)进行登入操作:
在这里插入图片描述
通过userDetailsService.loadUserByUsername(username)获取UserDetails对象,执行该方法会进入到SecurityConfig配置文件下的userDetailsService()中,从数据库中获取用户UmsAdmin信息和权限:
在这里插入图片描述
此时控制台输出结果:
在这里插入图片描述
从数据库中获取到的用户数据:
在这里插入图片描述
通过PasswordEncoder进行密码匹配
在这里插入图片描述
匹配成功,封装Authentication对象:
在这里插入图片描述
然后将Authentication保存到 SecurityContextHolder中
在这里插入图片描述
前面提到过UserDetails 会赋给认证通过的 Authentication 的 principal,确实如此
在这里插入图片描述
然后通过Token工具类生成Token字符串,并返回:
在这里插入图片描述
虽然传递的UserDetails对象,实际上只将用户名作为JWT的载荷( JWT中不要传入敏感信息 ):
在这里插入图片描述
在这里插入图片描述

最后收到返回的Token并将Token返回给前端:
在这里插入图片描述
前端收到服务端的Token信息:
在这里插入图片描述
前端收到token信息后将JWT的token信息放在请求头中,然后再去访问相应接口,这里访问http://localhost:8089/admin/permission/1接口

在这里插入图片描述
再次经过自定义过滤器的时候,就可以获取该请求头中的数据在这里插入图片描述
从请求头中获取关键信息,用户名和JWT的token:
在这里插入图片描述
如果解析出的username不为null,就通过UserDetailsService.loadUserByUsername(username)获取UserDetails:
在这里插入图片描述
一样的,执行UserDetailsService.loadUserByUsername(username)还是执行我们编写的userDetailsService() ,从数据库中获取

在这里插入图片描述
获取到的UserDetails对象
在这里插入图片描述

控制台输出信息:
在这里插入图片描述
然后通过JWT的工具类进行验证:
在这里插入图片描述
验证通过后封装Authenticationm,并将authentication保存在SecurityContextHolder中
在这里插入图片描述
然后放行执行其他过滤器:
在这里插入图片描述

同样的在业务层中实现业务代码,并返回给controller层:
在这里插入图片描述
controller层收到后返回给前端:
在这里插入图片描述

在这里插入图片描述

2.授权

(1) 加入权限到Authentication中

将权限信息写入到UserDetails中
在这里插入图片描述
再将UserDetails封装到Authentication中
在这里插入图片描述
AdminUserDetails是UserDetails的接口实现类
在这里插入图片描述

(2) SecurityConfig配置文件中开启注解权限配置

使用 @EnableGlobalMethodSecurity(prePostEnabled=true) 注解
在这里插入图片描述

(3) 给接口中的方法添加访问权限

使用 @PreAuthorize("hasAuthority('pms:brand:read')") 注解
在这里插入图片描述

重新运行后,访问 http://localhost:8089/admin/permission/1 接口 ( 不需要再登入,因为默认JWT失效时间为1周 ),由于该用户没有权限访问,所以访问失败:
在这里插入图片描述
接下来更换用户test,密码为123456进行登入:
在这里插入图片描述

在http://localhost:8089/admin/permission/1中更换请求头的token信息:
在这里插入图片描述
再次访问接口(访问成功):
在这里插入图片描述

(4) 用户权限表的建立

RBAC权限模型

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示:
在这里插入图片描述

在这里插入图片描述

前面项目中的权限数据表的设计就是采用RBAC模型,如下图所示( 数据库的设计来源于github上的mall项目,这里不做解释 ):
在这里插入图片描述

ums_role角色表,表中存在三种角色,商品管理员,订单管理员和超级管理员
在这里插入图片描述
在这里插入图片描述

ums_admin用户表:
在这里插入图片描述
在这里插入图片描述

ums_admin和ums_role关联表 ums_admin_role_relation:
在这里插入图片描述
ums_permission:许可表
在这里插入图片描述

在这里插入图片描述
ums_admin和ums_permission的关联表:ums_admin_permission_relation:
在这里插入图片描述
ums_role和ums_permission的关联表ums_role_permission_relation:
在这里插入图片描述

查询权限的sql语句如下:

        SELECT
            p.*
        FROM
            ums_admin_role_relation ar
                LEFT JOIN ums_role r ON ar.role_id = r.id
                LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
                LEFT JOIN ums_permission p ON rp.permission_id = p.id
        WHERE
            ar.admin_id = #{adminId}
          AND p.id IS NOT NULL
          AND p.id NOT IN (
            SELECT
                p.id
            FROM
                ums_admin_permission_relation pr
                    LEFT JOIN ums_permission p ON pr.permission_id = p.id
            WHERE
                pr.type = - 1
              AND pr.admin_id = #{adminId}
        )
        UNION
        SELECT
            p.*
        FROM
            ums_admin_permission_relation pr
                LEFT JOIN ums_permission p ON pr.permission_id = p.id
        WHERE
            pr.type = 1
          AND pr.admin_id = #{adminId}

前面的项目中,获取数据库的权限由于有多张表参与,所以无法直接使用mybatis生成的dao进行操作,所以需要自己创建dao,如下:
在这里插入图片描述
UmsAdminRoleRelationDao.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.dudu.dao.UmsAdminRoleRelationDao">
    <select id="getPermissionList" resultMap="com.dudu.mbg.mapper.UmsPermissionMapper.BaseResultMap">
        SELECT
            p.*
        FROM
            ums_admin_role_relation ar
                LEFT JOIN ums_role r ON ar.role_id = r.id
                LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
                LEFT JOIN ums_permission p ON rp.permission_id = p.id
        WHERE
            ar.admin_id = #{adminId}
          AND p.id IS NOT NULL
          AND p.id NOT IN (
            SELECT
                p.id
            FROM
                ums_admin_permission_relation pr
                    LEFT JOIN ums_permission p ON pr.permission_id = p.id
            WHERE
                pr.type = - 1
              AND pr.admin_id = #{adminId}
        )
        UNION
        SELECT
            p.*
        FROM
            ums_admin_permission_relation pr
                LEFT JOIN ums_permission p ON pr.permission_id = p.id
        WHERE
            pr.type = 1
          AND pr.admin_id = #{adminId}
    </select>
</mapper>

dao层中定义UmsAdminRoleRelationDao的接口
在这里插入图片描述
业务层中通过adminRoleRelationDao获取数据库中的权限信息
在这里插入图片描述

在这里插入图片描述
每一个mapper接口中都没有@mapper所以需要添加一个MyBatisConfig的配置类通过@MapperScan注解扫描mapper
在这里插入图片描述

3.自定义失败处理

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用 AccessDeniedHandler 对象的方法去进行异常处理。

(1) 创建异常处理类

在这里插入图片描述
RestAuthenticationEntryPoint类( 当未登录或者token失效访问接口时,自定义的返回结果【认证失败】 ):

package com.dudu.component;

import cn.hutool.json.JSONUtil;
import com.dudu.common.api.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果【认证失败】
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }
}

RestfulAccessDeniedHandler类( 当访问接口没有权限时,自定义的返回结果【授权失败】 ) :

package com.dudu.component;

import cn.hutool.json.JSONUtil;
import com.dudu.common.api.CommonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * 当访问接口没有权限时,自定义的返回结果【授权失败】
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

(2) 配置移除处理类

在SecurityConfig中configure()方法中配置异常处理类:

  • 先注入对应的处理器

在这里插入图片描述

  • 然后我们可以使用HttpSecurity对象的方法去配置。

在这里插入图片描述

4.跨域问题

  • 添加跨域配置类
package com.dudu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 全局跨域配置
 */
@Configuration
public class GlobalCorsConfig{

    /**
     * 允许跨域调用的过滤器
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许所有域名进行跨域调用
        config.addAllowedOriginPattern("*");
        //允许跨越发送cookie
        config.setAllowCredentials(true);
        //放行全部原始头信息
        config.addAllowedHeader("*");
        //允许所有请求方法跨域调用
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

  • 开启SpringSecurity的跨域访问

在这里插入图片描述

三.源码下载

JWT+SpringSecurity实现登入案例的代码采用github上的mall项目教程的代码,你可以通过该教程的链接进行下载,该教程路径https://www.macrozheng.com/mall/architect/mall_arch_05.html:
在这里插入图片描述
当然你也可以通过我的微信公众号进行下载,里面包括了博文中涉及到的所有代码,微信公众号搜索程序员孤夜(或扫描下方二维码),后台回复 登入案例 ,即可获取本篇文章所使用的源代码下载链接。
在这里插入图片描述

举报

相关推荐

0 条评论