本篇博文目录:
SpringSecurity 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。
SpringSecurity 是一个致力于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以如何轻松地扩展以满足自定义需求
官网地址:
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

SpringSecurity 是一个提供身份验证、授权和防止常见攻击的框架。它对保护命令式和反应式应用程序都提供了一流的支持,是保护基于 Spring 的应用程序的事实上的标准。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
Spring 无缝整合,全面的权限控制,专门为 Web 开发而设计(旧版本不能脱离 Web 环境使用,新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境),重量级。
功能,社区资源上SpringSecurity远远优于Shiro,但是Shiro是轻量级更加容易上手,在SSM框架中整合 Spring Security 比较麻烦,但是在SpringBoot项目中 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security,所以推荐在SSM框架中使用Shiro,在SpringBoot和SpringCloud中使用SpringSecurity,你可以通过这篇博文了解这二种方式的实现SpringBoot学习—SpringSecurity与Shiro。
SpringSecurity 为身份验证提供了全面的支持。身份验证是我们验证试图访问特定资源的用户身份的方式。对用户进行身份验证的一种常见方法是要求用户输入用户名和密码。一旦执行了身份验证,我们就知道了身份并可以执行授权。
SpringSecurity 内置了对用户身份验证的支持。
创建一个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接口了:

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,下图列出的是该过滤链中比较重要的几个过滤器。

上图中的三个过滤器分别是:
① UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
② ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
③ FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部
在项目启动类中,按下图所示进行debug,就可以观察到这个过滤链,过滤链上一共有16个过滤器( run.getBean(DefaultSecurityFilterChain.class) )

这16个过滤器分别是:

📢 注意,下面几个知识点来源于这篇博文:https://www.w3cschool.cn/springsecurity/ted11ii1.html,如果需要更详细的了解请查阅原文。
Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext。因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的。这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。
UserDetailsService是一个加载用户特定数据的核心接口,登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。
UserDetails是一个提供核心用户信息的接口,通过 UserDetailsService 的 loadUserByUsername() 方法获取,然后将该 UserDetails 赋给认证通过的 Authentication 的 principal。
常用方法:
| 方法名 | 解释 |
|---|---|
| Collection<?extendsGrantedAuthority> getAuthorities(); | 表示获取登录用户所有权限 |
| String getPassword(); | 表示获取密码 |
| String getUsername(); | 表示获取用户名 |
| boolean isAccountNonExpired(); | 表示判断账户是否过期 |
| boolean isAccountNonLocked(); | 表示判断账户是否被锁定 |
| boolean isCredentialsNonExpired(); | 表示凭证{密码}是否过期 |
| boolean isEnabled(); | 表示当前用户是否可用 |
Spring Security 的PasswordEncoder接口用于执行密码的单向转换,以允许安全地存储密码。给定PasswordEncoder是单向转换,当密码转换需要双向(即存储用于对数据库进行身份验证的凭据)时,并不打算这样做。
通常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算法来散列密码,是一种故意缓慢的算法,需要大量内存 |
SpringSecurity5.x版本默认的PasswordEncoder方式改成了DelegatingPasswordEncoder委托类,这是因为在5.0之前默认采用的NoOpPasswordEncoder,存在如下问题:
SpringSecurity引入了DelegatingPasswordEncoder,它通过以下方式解决了所有问题:
DelegatingPasswordEncoder委托支持动态的多种密码加密方式,它内部其实是一个Map集合,根据传递的Key(Key为加密方式)获取Map集合的Value,而Value则是具体的PasswordEncoder实现类。
方式1:创建默认的代理 PasswordEncoder(SpringSecurity5.x)

我们来看看createDelegatingPasswordEncoder()的源码:
从下图我们可以看出使用PasswordEncoderFactories.createDelegatingPasswordEncoder()创建的PasswordEncoder,默认加密方式是bcrypt,PasswordEncoder的默认实现类是BCryptPasswordEncoder

运行效果:
从下图我们可以看出密码的一般格式为:
{id}encodedPassword,如果使用的NoOpPasswordEncoder编码的话(不推荐使用该编码),数据库中的密码格式为{noop}密码,如 {noop}123456 。

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

运行效果:

实际项目中如果不采用默认方式,可以通过@Bean的方式来统一配置全局共用的PasswordEncoder,如下所示:

上图所示在配置中还重新配置了users

就一个用户,用户名为user,密码123456,使用该用户进行登入:

登入成功:

📢 注意,下面的图来源于https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1,并且该视频详细讲解了JWT+SpringSecurity的知识,如果想更详细的了解,跳转观看即可,关于JWT的知识你可以通过这篇博文->JWT详细讲解(保姆级教程)进行学习。
SpringSecurity的认证流程图如下,如果不按默认方式进行,将对应接口的实现类换成自己的实现类即可:

JWT的流程图如下:

JWT+SpringSecurity实现登入案例:
案例来源于github上mall项目的学习教程,这个案例的数据并没有从Redis中获取,而是从数据库中获取,之所以说没有用Redis进行获取是因为前面推荐的https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1视频就是采用Redis进行存储。

下面给出关键代码来了解这个认证流程
项目中有一个SecurityConfig类,表示SpringSecurity的配置

这个configure配置是整个Security Config的配置(过滤器链的配置),下图红框中的配置表示在UsernamePasswordAuthenticationFilter过滤器前添加一个过滤器。

这个过滤器是继承OncePerRequestFilter过滤器(之所以继承 OncePerRequestFilter 因为OncePerRequestFilter 一个请求只被过滤器拦截一次。请求转发不会第二次触发过滤器 ,而Filter会触发二次)

该过滤器存放在component文件下的JwtAuthenticationTokenFilter类

程序运行后,过滤器会一个一个的执行当执行到UsernamePasswordAuthenticationFilter过滤器之前会先执行自定义的JwtAuthenticationTokenFilter过滤器中的doFilterInternal()方法:

该方法业务逻辑非常清晰,就是判断是否在请求头中存在JWT的Token,如果存在( 用户已经登入过了 )就从token的载荷中获取用户名,然后再根据UserDetaulsService的loadUserByUsername(username)获取用户信息 (UserDetaulsService被替换成从数据库中获取用户信息,详细代码如下图所示 ) 然后进行认证,如果不存在( 该请求为登入请求 ),用户的信息从登入界面获取( 用户名,密码 )然后进行认证。

这里的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层收到后返回给前端:


在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,具体操作如下:
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication中就可以了,在前面的项目中我们已经将权限写入到Authentication中了,就是在获取UserDetails的时候,将权限信息也写入到UserDetails中,然后再封装到Authentication中,整个过程如下:
将权限信息写入到UserDetails中

再将UserDetails封装到Authentication中

AdminUserDetails是UserDetails的接口实现类

使用 @EnableGlobalMethodSecurity(prePostEnabled=true) 注解

使用 @PreAuthorize("hasAuthority('pms:brand:read')") 注解

重新运行后,访问 http://localhost:8089/admin/permission/1 接口 ( 不需要再登入,因为默认JWT失效时间为1周 ),由于该用户没有权限访问,所以访问失败:

接下来更换用户test,密码为123456进行登入:

在http://localhost:8089/admin/permission/1中更换请求头的token信息:

再次访问接口(访问成功):

RBAC权限模型
基于角色的访问控制(RBAC)是实施面向企业安全策略的一种有效的访问控制方式。其基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。博文推荐:RBAC权限模型——项目实战
在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示:

RBAC其实是一种分析模型,主要分为:基本模型RBAC0(Core RBAC)、角色分层模型RBAC1(Hierarchal RBAC)、角色限制模型RBAC2(Constraint RBAC)和统一模型RBAC3(Combines RBAC),其中RBAC0为核心,其他模型在RBAC0基础上进行扩展,
RBAC0由用户,角色,会话和许可(“操作”和“控制对象”)四部分组成,如下图所示:

前面项目中的权限数据表的设计就是采用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

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
AuthenticationEntryPoint 对象的方法去进行异常处理。AccessDeniedHandler 对象的方法去进行异常处理。所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可,具体操作如下:

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();
}
}
在SecurityConfig中configure()方法中配置异常处理类:


浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 (前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。),如何设置跨域,你可以通过这篇博文springboot设置Cors跨域的四种方式进行学习。
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配置文件的configure(HttpSecurity httpSecurity)方法下通过httpSecurity.cors()运行跨域:

JWT+SpringSecurity实现登入案例的代码采用github上的mall项目教程的代码,你可以通过该教程的链接进行下载,该教程路径https://www.macrozheng.com/mall/architect/mall_arch_05.html:

当然你也可以通过我的微信公众号进行下载,里面包括了博文中涉及到的所有代码,微信公众号搜索程序员孤夜(或扫描下方二维码),后台回复 登入案例 ,即可获取本篇文章所使用的源代码下载链接。

无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主
1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva
目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式
有点边缘情况,但知道为什么&&=会这样吗?我正在使用1.9.2。obj=Object.newobj.instance_eval{@bar&&=@bar}#=>nil,expectedobj.instance_variables#=>[],soobjhasno@barinstancevariableobj.instance_eval{@bar=@bar&&@bar}#ostensiblythesameas@bar&&=@barobj.instance_variables#=>[:@bar]#whywouldthisversioninitialize@bar?为了比较,||=将实例变量初始
目录一、安装包链接二、安装详细步骤1.安装Wireshark和WinPcap2.安装OracleVMVirtualBox3.安装ensp三、安装后注册四、启动路由器出现40错误怎么解决一、安装包链接二、安装详细步骤链接:https://pan.baidu.com/s/1QbUUYMOMIV2oeIKHWP1SpA?pwd=xftx提取码:xftx1.安装Wireshark和WinPcap找到Wireshark安装包所在文件夹,双击它,按照以下步骤安装。2.安装OracleVMVirtualBox找到OracleVMVirtualBox安装包所在文件夹,双击它,按照以下步骤安装。注:可自定义安装
Nginx安装1.官网下载Nginx2.使用XShell和Xftp将压缩包上传到Linux虚拟机中3.解压文件nginx-1.20.2.tar.gz4.配置nginx5.启动nginx6.拓展(修改端口和常用命令)(一)修改nginx端口(二)常用命令1.官网下载Nginxhttp://nginx.org/en/download.html这里我下载的是1.20.2版本,大家按需下载对应稳定版即可2.使用XShell和Xftp将压缩包上传到Linux虚拟机中没有XShell可以参考《Linux操作系统CentOS7连接XShell》3.解压文件nginx-1.20.2.tar.gz1)检查是否存
因学习需要用到keras,通过查找较多资料最终完成Anaconda、TensorFlow和Keras的简单安装。因为网上的相关资料较多但大部分不够全面,查找起来不太方便,因此自己记录一下成功下载安装的详细过程,顺便推荐一下借鉴的写的很好的相关教程文章。keras需要在TensorFlow之上才能运行,所以要先安装TensorFlow,而TensorFlow只能在3.7以前的python版本中运行,所以需要先创建一个基于python3.6的虚拟环境,因此便需要先下载Anaconda。一、Anaconda3下载和安装Anaconda下载安装教程原文链接:https://blog.csdn.net/
【动态规划】一、背包问题1.背包问题总结1)动规四部曲:2)递推公式总结:3)遍历顺序总结:2.01背包1)二维dp数组代码实现2)一维dp数组代码实现3.完全背包代码实现4.多重背包代码实现一、背包问题1.背包问题总结暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!背包问题是动态规划(DynamicPlanning)里的非常重要的一部分,关于几种常见的背包,其关系如下:在解决背包问题的时候,我们通常都是按照如下五部来逐步分析,把这五部都搞透了,算是对动规来理解深入了。1)动规四部曲:(1)确定dp数组及其下标的含义(2)确定递推公式(3)dp数组的初始化(4)确定遍历顺