【SpringSecurity】springboot整合SpringSecurity实现登录校验与权限认证
- 【一】SpringSecurity框架简介
- 【二】SpringSecurity与shiro
- 【三】SpringSecurity过滤器
- 【1】SpringSecurity中常见的过滤器
- 【2】15种过滤器
- (1)WebAsyncManagerIntegrationFilter
- (2)SecurityContextPersistenceFilter
- (3)HeaderWriterFilter
- (4)CsrfFilter
- (5)LogoutFilter
- (6)UsernamePasswordAuthenticationFilter
- (7)DefaultLoginPageGeneratingFilter
- (8)DefaultLogoutPageGeneratingFilter
- (9)BasicAuthenticationFilter
- (10)RequestCacheAwareFilter
- (11)SecurityContextHolderAwareRequestFilter
- (12)AnonymousAuthenticationFilter
- (13)SessionManagementFilter
- (14)ExceptionTranslationFilter
- (15)FilterSecurityInterceptor
- 【3】SpringSecurity整体流程概述
- 【4】详细流程步骤
- 【四】PasswordEncoder接口重点介绍
- 【五】整合SpringSecurity实现登录校验与权限认证
- 【六】微服务认证与授权实现思路
- 【七】微服务代码实例
【一】SpringSecurity框架简介
关于安全方面的两个主要区域是“认证”和“授权”(或者说是访问控制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是SpringSecurity重要核心功能。
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录。
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
【二】SpringSecurity与shiro
【1】SpringSecurity特点
(1)与Spring框架无缝整合
(2)全面的权限控制
(3)专门为Web开发而设计
旧版本不能脱离Web环境使用
新版本对整个框架进行了分层抽取,分成了核心模块和Web模块,单独引入了核心模块就可以脱离Web环境
重量级
【2】shiro特点
Apache旗下的轻量级权限控制框架
(1)轻量级
shiro主张的理念是把复杂的事情变简单,针对性能更高要求的互联网应用有更好的变现
(2)通用性
好处:不局限于Web环境,可以脱离Web环境使用
缺陷:在Web环境下一些特定的需求需要手动编写代码定制
【3】SpringSecurity和shiro总结
相对于shiro,在SSM中整合SpringSecurity都是比较麻烦的操作,所以,SpringSecurity虽然功能比shiro强大,但是使用反而没有shiro多,(shiro虽然功能没有SpringSecurity多,但是对于大部分项目而言,shiro也够用了)。自从有了Springboot之后,Springboot对于SpringSecurity提供了自动化配置方案,可以使用更少的配置来使用SpringSecurity。因此,一般来说,常见的安全管理技术栈的组合是这样的:
(1)SSM+shiro
(2)Springboot/SpringCloud+SpringSecurity
以上只是一个推荐的组合而已,如果单从技术上来说,无论怎么组合,都是可以运行的。
【三】SpringSecurity过滤器
【1】SpringSecurity中常见的过滤器

【2】15种过滤器
SpringSecurity采用的是责任链的设计模式,它有一条很长的过滤器链。这些过滤器按特定顺序执行,每个过滤器负责不同的安全任务,如身份验证、授权、会话管理等。下面介绍一些重要的过滤器及其功能:
(1)WebAsyncManagerIntegrationFilter
功能:将 Spring Security 上下文与 Spring 的 WebAsyncManager 集成,确保异步请求也能正确处理安全上下文。
位置:通常位于过滤器链的最前端。
(2)SecurityContextPersistenceFilter
功能:在每个请求开始时,从 HttpSession 中获取安全上下文(SecurityContext),并将其设置到当前线程中;在请求结束时,将安全上下文保存回 HttpSession 中。
位置:在请求处理的早期阶段执行。
(3)HeaderWriterFilter
功能:用于向响应头中添加安全相关的头部信息,如 X-Frame-Options、X-XSS-Protection 等,增强应用的安全性。
位置:在安全上下文设置之后执行。
(4)CsrfFilter
功能:防止跨站请求伪造(CSRF)攻击,验证请求中的 CSRF 令牌是否有效。
位置:在处理表单提交等敏感请求之前执行。
(5)LogoutFilter
功能:处理用户的注销请求,清除安全上下文、销毁会话等。
位置:在处理注销相关的 URL 请求时执行。
(6)UsernamePasswordAuthenticationFilter
功能:处理基于表单的用户名和密码认证,从请求中提取用户名和密码,尝试进行身份验证。
位置:通常在处理登录表单提交的 URL 时执行。
(7)DefaultLoginPageGeneratingFilter
功能:如果没有自定义登录页面,该过滤器会生成一个默认的登录页面。
位置:在处理登录相关请求时,若没有自定义登录页面则会起作用。
(8)DefaultLogoutPageGeneratingFilter
功能:如果没有自定义注销页面,该过滤器会生成一个默认的注销页面。
位置:在处理注销相关请求时,若没有自定义注销页面则会起作用。
(9)BasicAuthenticationFilter
功能:处理基于 HTTP Basic 认证的请求,从请求头中提取基本认证信息进行身份验证。
位置:在处理需要 Basic 认证的请求时执行。
(10)RequestCacheAwareFilter
功能:处理请求缓存,当用户在未认证的情况下访问受保护资源时,会缓存该请求,认证成功后重定向到原请求的资源。
位置:在认证前后处理请求缓存相关操作。
(11)SecurityContextHolderAwareRequestFilter
功能:将 HttpServletRequest 包装成 SecurityContextHolderAwareRequestWrapper,提供额外的安全相关方法。
位置:在请求处理过程中,对请求进行包装。
(12)AnonymousAuthenticationFilter
功能:如果请求在经过前面的过滤器后仍未认证,该过滤器会为请求设置一个匿名身份,避免后续处理因缺少身份信息而出错。
位置:在前面的认证过滤器之后执行。
(13)SessionManagementFilter
功能:管理用户会话,处理会话超时、并发会话控制等问题。
位置:在会话相关操作的处理阶段执行。
(14)ExceptionTranslationFilter
功能:捕获认证和授权过程中抛出的异常,并将其转换为合适的 HTTP 响应,如重定向到登录页面或返回 403 状态码。
位置:在认证和授权过滤器之后,处理异常情况。
(15)FilterSecurityInterceptor
功能:进行最终的授权检查,根据配置的访问规则判断用户是否有权限访问请求的资源。
位置:位于过滤器链的末尾,在所有其他过滤器执行完毕后进行最终的授权决策。
【3】SpringSecurity整体流程概述
当一个客户端发起请求时,请求会进入 Spring Security 的过滤器链。过滤器链中的各个过滤器会依次对请求进行处理,其中涉及认证的过滤器会尝试对用户进行身份验证。如果认证成功,用户的身份信息会被存储在安全上下文中;如果认证失败,则会根据配置进行相应的错误处理。
【4】详细流程步骤
(1)请求进入过滤器链
客户端发起请求后,请求首先会到达 Spring Security 的过滤器链。Spring Security 默认有多个过滤器,这些过滤器按特定顺序排列,每个过滤器负责不同的安全任务。例如,SecurityContextPersistenceFilter 是过滤器链中的第一个过滤器,它会在请求开始时从 HttpSession 中获取安全上下文(SecurityContext),并将其设置到当前线程中;在请求结束时,将安全上下文保存回 HttpSession 中。
(2)认证过滤器处理
不同类型的认证方式由不同的认证过滤器处理,以下是几种常见的认证方式及其对应的过滤器:
(1)表单登录认证(UsernamePasswordAuthenticationFilter)
请求匹配:当请求的 URL 匹配到配置的登录 URL(默认为 /login),且请求方法为 POST 时,UsernamePasswordAuthenticationFilter 会开始工作。
提取认证信息:该过滤器会从请求中提取用户名和密码,通常是从表单的 username 和 password 字段中获取。
创建认证令牌:使用提取的用户名和密码创建一个 UsernamePasswordAuthenticationToken 对象,该对象实现了 Authentication 接口,用于封装用户的认证信息。
调用认证管理器:将创建的 UsernamePasswordAuthenticationToken 对象传递给 AuthenticationManager 进行认证。
(2)HTTP Basic 认证(BasicAuthenticationFilter)
请求匹配:当请求头中包含 Authorization 字段,且值以 Basic 开头时,BasicAuthenticationFilter 会对请求进行处理。
提取认证信息:从 Authorization 字段中提取基本认证信息(通常是经过 Base64 编码的用户名和密码),并进行解码。
创建认证令牌:使用解码后的用户名和密码创建 UsernamePasswordAuthenticationToken 对象。
调用认证管理器:将认证令牌传递给 AuthenticationManager 进行认证。
(3)认证管理器(AuthenticationManager)处理
AuthenticationManager 是一个接口,它的主要职责是对传入的 Authentication 对象进行认证。默认实现是 ProviderManager,它内部维护了一个 AuthenticationProvider 列表。
1-遍历认证提供者:ProviderManager 会遍历 AuthenticationProvider 列表,依次调用每个 AuthenticationProvider 的 authenticate 方法,直到找到能够处理该 Authentication 对象的 AuthenticationProvider。
2-认证处理:AuthenticationProvider 会根据具体的认证逻辑对 Authentication 对象进行验证,例如查询数据库验证用户名和密码是否匹配。如果验证成功,会返回一个已认证的 Authentication 对象;如果验证失败,会抛出相应的异常。
(4)用户详情服务(UserDetailsService)
在认证过程中,AuthenticationProvider 通常会调用 UserDetailsService 来获取用户的详细信息。UserDetailsService 是一个接口,其主要方法是 loadUserByUsername,该方法根据用户名从数据源(如数据库、LDAP 等)中加载用户的详细信息,返回一个 UserDetails 对象。UserDetails 接口封装了用户的核心信息,如用户名、密码、权限等。
(5)认证结果处理
认证成功:如果 AuthenticationProvider 认证成功,会返回一个已认证的 Authentication 对象,其中包含用户的详细信息和权限。UsernamePasswordAuthenticationFilter 或 BasicAuthenticationFilter 会将该对象设置到安全上下文中(SecurityContextHolder),表示用户已成功认证。
认证失败:如果认证失败,AuthenticationProvider 会抛出相应的异常,如 BadCredentialsException(用户名或密码错误)。ExceptionTranslationFilter 会捕获这些异常,并根据配置进行相应的处理,例如重定向到登录页面或返回 401 未授权状态码。
(6)后续请求处理
认证成功后,用户的身份信息会存储在安全上下文中。后续的请求会通过 SecurityContextPersistenceFilter 从 HttpSession 中恢复安全上下文,确保用户在整个会话期间保持认证状态。同时,FilterSecurityInterceptor 会根据配置的访问规则对请求进行授权检查,判断用户是否有权限访问请求的资源。
【四】PasswordEncoder接口重点介绍
PasswordEncoder 是 Spring Security 框架里用于密码编码和解码验证的核心接口。它提供了对密码进行加密存储以及验证用户输入密码是否匹配已存储加密密码的功能,下面从几个方面详细介绍该接口。
【1】接口作用
在应用程序里,密码不应以明文形式存储在数据库中,这是因为一旦数据库泄露,用户密码会直接暴露,造成严重安全问题。PasswordEncoder 接口就是为了解决此问题而设计,它能对用户输入的密码进行加密处理,然后将加密后的密码存储到数据库中。当用户登录时,它会对用户输入的明文密码进行相同加密处理,再和数据库中存储的加密密码进行比较,以此验证密码是否正确。
【2】核心方法
PasswordEncoder 接口定义了两个核心方法:
(1)String encode(CharSequence rawPassword):加密处理
该方法接收一个明文密码(CharSequence 类型),对其进行加密处理,最后返回加密后的字符串。
(2)boolean matches(CharSequence rawPassword, String encodedPassword):密码校验
这个方法接收一个明文密码和一个已加密的密码,对明文密码进行相同加密处理后,将其与已加密的密码作比较,若匹配则返回 true,反之返回 false。
【3】常见实现类
Spring Security 提供了多个 PasswordEncoder 接口的实现类,下面是一些常见的实现类:
(1)BCryptPasswordEncoder:使用 BCrypt 强哈希函数对密码进行加密,每次加密相同的明文密码都会生成不同的加密结果,安全性较高,是 Spring Security 推荐使用的密码编码器。
(2)StandardPasswordEncoder:采用 SHA - 256 哈希算法对密码进行加密,需要一个盐值(salt)来增强安全性。
(3)NoOpPasswordEncoder:不进行任何加密处理,直接返回明文密码,仅用于开发和测试环境,绝对不能在生产环境中使用。
【4】代码示例
以下是使用 BCryptPasswordEncoder 的示例代码:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordEncoderExample {
public static void main(String[] args) {
// 创建 BCryptPasswordEncoder 实例
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 明文密码
String rawPassword = "123456";
// 对明文密码进行加密
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Encoded Password: " + encodedPassword);
// 验证密码
boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("Password Matches: " + isMatch);
}
}
代码解释
(1)首先创建了 BCryptPasswordEncoder 实例。
(2)接着定义了一个明文密码 “123456”。
(3)利用 encode 方法对明文密码进行加密,将加密后的结果存储在 encodedPassword 变量中并打印输出。
(4)最后使用 matches 方法验证明文密码和加密后的密码是否匹配,并将结果打印输出。
【5】BCryptPasswordEncoder和其他加密算法(跳转)
加密算法详解:
加密算法详解
【五】整合SpringSecurity实现登录校验与权限认证
创建一个spring boot项目,并导入一些初始依赖,不赘述
【1】身份认证
(1)controller测试
由于我们加入了 spring-boot-starter-security 的依赖,所以security就会自动生效了。这时直接编写一个controller控制器,并编写一个接口进行测试:

可以看到我们在访问这个接口时出现了拦截,必须要我们进行登录之后才能访问;

(2)登录认证流程
Spring Security 6.x 的认证实现流程如下:
(1)用户提交登录请求
(2)Spring Security 将请求交给 UsernamePasswordAuthenticationFilter 过滤器处理。
(3)UsernamePasswordAuthenticationFilter 获取请求中的用户名和密码,并生成一个 AuthenticationToken 对象,将其交给 AuthenticationManager 进行认证。
(4)AuthenticationManager 通过 UserDetailsService 获取用户信息,然后使用 PasswordEncoder 对用户密码进行校验。
(5)如果密码正确,AuthenticationManager 会生成一个认证通过的 Authentication 对象,并返回给 UsernamePasswordAuthenticationFilter 过滤器。如果密码不正确,则 AuthenticationManager 抛出一个 AuthenticationException 异常。
(6)UsernamePasswordAuthenticationFilter 将 Authentication 对象交给 SecurityContextHolder 进行管理,并调用 AuthenticationSuccessHandler 处理认证成功的情况。
(7)如果认证失败,UsernamePasswordAuthenticationFilter 会调用 AuthenticationFailureHandler 处理认证失败的情况。
看起来有点复杂,其实写起来很简单的。spring security的底层就是一堆的过滤器来是实现的,而我们只需要编写一些重要的过滤器即可,其他的就用spring security默认的实现,只要不影响我们正常的登录功能即可。
(3)创建一个用户表
创建一个用户表用来进行登录实现,注意这个表中的用户名不能重复,我们将用户名作为每一个用户的唯一凭证,就如同人的手机号或者身份证号一样。

实体类、mapper、service、controller等基本配置不赘述
(4)创建一个MyUserDetailsService类
创建一个MyUserDetailsService类来实现SpringSecurity的UserDetailsService接口(这里进行用户登录和授权的逻辑处理)
UserDetailsService:此接口中定义了登录服务方法,用来实现登录逻辑。方法的返回值是UserDetails,也是spring security框架定义中的一个接口,用来存储用户信息,我们可以自定义一个类用来实现这个接口,将来返回的时候就返回我们自定义的用户实体类。
(1)实现UserDetailsService接口
@Component
public class MyUserDetailsService implements UserDetailsService {
/*
* UserDetailsService:提供查询用户功能,如根据用户名查询用户,并返回UserDetails
*UserDetails,SpringSecurity定义的类, 记录用户信息,如用户名、密码、权限等
* */
@Autowired
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库中查询用户
SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(username != null, SysUser::getUsername, username));
if (sysUser==null){
throw new UsernameNotFoundException("用户不存在");
}
// 封装查询到的用户信息
MySysUserDetails mySysUserDetails=new MySysUserDetails(sysUser);
return mySysUserDetails;
}
}
(2)实现UserDetails接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MySysUserDetails implements UserDetails {
private Integer id;
private String username;
private String password;
// 用户拥有的权限集合,我这里先设置为null,将来会再更改的
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public MySysUserDetails(SysUser sysUser) {
this.id = sysUser.getId();
this.username = sysUser.getUsername();
this.password = sysUser.getPassword();
}
// 后面四个方法都是用户是否可用、是否过期之类的。我都设置为true
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
(5)通过配置类对AuthenticationManager与自定义的UserDetails和PasswordEncoder进行关联
Spring Security是通过AuthenticationManager实现的认证,会借此来判断用户名和密码的正确性
密码解析器spring security框架定义的接口:PasswordEncoder
spring security框架强制要求,必须在spring容器中存在PasswordEncoder类型对象,且对象唯一
@Configuration
@EnableWebSecurity //开启webSecurity服务
public class SecurityConfig {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Bean
public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){
DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
//将编写的UserDetailsService注入进来
provider.setUserDetailsService(myUserDetailsService);
//将使用的密码编译器加入进来
provider.setPasswordEncoder(passwordEncoder);
//将provider放置到AuthenticationManager 中
ProviderManager providerManager=new ProviderManager(provider);
return providerManager;
}
/*
* 在security安全框架中,提供了若干密码解析器实现类型。
* 其中BCryptPasswordEncoder 叫强散列加密。可以保证相同的明文,多次加密后,
* 密码有相同的散列数据,而不是相同的结果。
* 匹配时,是基于相同的散列数据做的匹配。
* Spring Security 推荐使用 BCryptPasswordEncoder 作为密码加密和解析器。
* */
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
(6)在登录方法所在的类中注入AuthenticationManager
调用authenticate实现认证逻辑,并且在认证之后返回认证过的用户信息:
(1)controller层
// 用户登录
@PostMapping("/login")
public String login(@RequestBody LoginDto loginDto){
String token= sysUserService.login(loginDto);
return token;
}
(2)对应的service层的方法
编写具体的登录方法,创建一个UsernamePasswordAuthenticationToken对象,并传入相应的用户名和密码;注入一个AuthenticationManager的bean,这个bean是spring security封装的用来进行认证的类,调用这个类的authenticate方法并传入UsernamePasswordAuthenticationToken对象;
@Autowired
private AuthenticationManager authenticationManager;
// 登录接口的具体实现
@Override
public String login(LoginDto loginDto) {
// 传入用户名和密码
UsernamePasswordAuthenticationToken usernamePassword =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
//是实现登录逻辑,此时就会去调用LoadUserByUsername方法
Authentication authenticate = authenticationManager.authenticate(usernamePassword);
//获取返回的用户信息
Object principal = authenticate.getPrincipal();
//强转为MySysUserDetails类型
MySysUserDetails mySysUserDetails = (MySysUserDetails) principal;
//输出用户信息
System.err.println(mySysUserDetails);
//返回token
String token= UUID.randomUUID().toString();
return token;
}
(7)介绍UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken是Spring Security中用于表示基于用户名和密码的身份验证令牌的类。它主要有以下两个构造方法:
(1)UsernamePasswordAuthenticationToken(Object principal, Object credentials)
1-principal参数表示认证主体,通常是用户名或用户对象。在身份验证过程中,这通常是用来标识用户的信息,可以是用户名、邮箱等。
2-credentials参数表示凭据,通常是用户的密码或其他凭证信息。在身份验证过程中,这用于验证用户的身份。
(2)UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
1-除了上述两个参数外,这个构造方法还接受一个授权权限集合(authorities参数)。这个集合表示用户所拥有的权限,通常是一个包含用户权限信息的集合。
2-GrantedAuthority接口代表了用户的权限信息,可以通过该接口的实现类来表示用户具体的权限。
这两个构造方法的作用是创建一个包含用户身份信息、凭据信息和权限信息的身份验证令牌,以便在Spring Security中进行身份验证和授权操作。通过这些构造方法,可以将用户的相关信息封装成一个完整的身份验证对象,方便在安全框架中进行处理和验证。
总之,UsernamePasswordAuthenticationToken是在Spring Security中用于表示用户名密码身份验证信息的重要类,通过不同的构造方法可以满足不同场景下的需求。
(8)测试登录
造一些用户数据,并进行测试
访问:访问http://localhost:8080/test,自动跳转到了Spring Security提供的默认的登录页面;这是因为Spring Security默认所有的请求都要先登录才行,我们在这里登录之后就可以继续访问test页面了;
这里的用户名和密码就是我们在数据库中存储的用户名和密码

既然这个test请求要先进行拦截认证才能访问,那么,我们刚才编写的登录接口sys-user/login岂不是也要先进行拦截认证才能访问,这就与我们编写登录接口的初衷违背了,我们这个接口就是用来登陆的,现在还要先登录认证,之后再访问这个登录接口。那么有没有一种方法,不使用SpringSecurity默认的登录页面呢,使我们编写的登录接口所有人都可以直接访问呢?
(9)SecurityFilterChain 过滤器
配置用户登录的接口可以暴露出来,被所有人都正常的访问,不会被拦截转跳到默认登录页面,而是跳到自定义的登录页面。
在第二步设置的SecurityConfig类中设置过滤器:
(1)在spring security6.x版本之后,原先经常用的and()方法被废除了,现在spring官方推荐使用Lambda表达式的写法。
(2)因为我们接下来要进行测试,所以禁用CSRF保护
/*
* 配置权限相关的配置
* 安全框架本质上是一堆的过滤器,称之为过滤器链,每一个过滤器链的功能都不同
* 设置一些链接不要拦截
* */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
//关闭csrf
httpSecurity.csrf(it->it.disable());
httpSecurity.authorizeHttpRequests(it->
it.requestMatchers("/login","/sys-user/login").permitAll() //设置登录路径所有人都可以访问
.anyRequest().authenticated() //其他路径都要进行拦截
);
//表单
httpSecurity.formLogin(from->
from.loginPage("/login") //跳转到自定义的登录页面
.loginProcessingUrl("/sys-user/login") //处理前端的请求,与from表单的action一致即可
.defaultSuccessUrl("/index") //默认的请求成功之后的跳转页面,直接访问登录页面
);
return httpSecurity.build();
}
配置对应的controller
@Controller
public class Login {
@GetMapping("/login")
public String login(){
System.out.println("用户进入登录页面");
return "login"; //没使用json返回,直接映射到自定义登录的页面
}
@GetMapping("/index")
@ResponseBody
public String index(){
return "用户登录成功";
}
}
(10)测试登录
(1)访问test请求:遇到拦截,说明我们的配置生效了

(2)访问login请求,并用账号密码登陆成功
登录之后,会跳转到/test请求地址

现在我们直接访问/login登录页面:可以看到返回了/index页面的内容(这个是我们设置的默认登录成功之后返回的页面)

(11)测试退出登录
需要注意的是在Spring Security中,没有专门用于处理退出失败的接口。退出(注销)操作通常是由浏览器发起的,Spring Security会拦截注销请求并执行相应的注销逻辑。
退出操作通常是通过调用SecurityContextLogoutHandler来完成的,它会清除用户的安全上下文,包括认证信息和会话信息。
在security框架中,默认提供了退出登陆的功能。请求地址是 /lohout 此为默认值,可以通过配置进行修该。直接请求 /logout ,会实现自动退出登录逻辑(默认的/logout接收get、和post请求)
退出登陆时,会清楚内存中的登录用户主体信息,销毁会话对象等等。
自定义退出接口:
httpSecurity.logout(logout->{
logout.logoutUrl("/user/login") //自定义退出接口
.logoutSuccessHandler(logoutSuccess); //退出成功之后的逻辑
});
编写退出成功之后的逻辑,我们可以在这里删除掉redis中的数据,清除登录的上下文,设置返回的信息等等…(如果是前后端分离状态下的spring security,这些工作都可以在自定义的退出接口中进行实现。如果是前后端不分离的表单式登录,还是使用传统的Cookie和Session来进行用户信息的保存,我们自需要调用ogout.logoutUrl(“/user/login”) 方法来指定退出路径即可,退出的逻辑不需要我们来实现。)
@Component
public class LogoutSuccess implements LogoutSuccessHandler {
@Resource
private RedisTemplate<String,String> redisTemplate;
/*
* 登录成功之后的逻辑
* */
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
// 删除redis中的数据
redisTemplate.delete(token);
Map<String,Object> map=new HashMap<>();
map.put("msg","退出成功");
map.put("code",200);
response.getWriter().write(JSON.toJSONString(map));
response.setContentType("application/json;charset=utf-8");
}
}
【2】权限校验
(1)角色与权限
角色与权限在SpringSecurity中的作用:
(1)角色(Role)
角色是一组权限的集合,通常代表着用户的身份或职责。在Spring Security中,可以通过配置将角色分配给用户或者用户组,以此来控制用户对系统资源的访问。例如,管理员拥有添加、删除和修改用户的权限,而普通用户只能查看自己的信息。
(2)权限(Permission)
权限是指对某一特定资源的访问控制,例如读写文件、访问数据库等。在Spring Security中,通常使用“资源-操作”命名方式来定义权限,例如“/admin/* - GET”表示允许访问以/admin/开头的所有URL的GET请求。可以将权限分配给角色,也可以将其分配给单独的用户。
(2)角色表与权限表
角色与权限之间的关系是多对多的,建立两张简单的表;一张用来存放角色、一张用来存放权限
(1)角色表

(2)权限表

其他代码自动生成,不赘述
(3)权限认证流程
SpringSecurity要求将身份认证信息存到GrantedAuthority对象列表中。代表了当前用户的权限。 GrantedAuthority对象由AuthenticationManager插入到Authentication对象中,然后在做出授权决策 时由AccessDecisionManager实例读取。 GrantedAuthority 接口只有一个方法
String getAuthority();
AuthorizationManager实例通过该方法来获得GrantedAuthority。通过字符串的形式表示, GrantedAuthority可以很容易地被大多数AuthorizationManager实现读取。如果GrantedAuthority不 能精确地表示为String,则GrantedAuthorization被认为是复杂的,getAuthority()必须返回null
直接在登录时查询用户的权限,并放在我们自定义的实现了UserDetail的接口类中,用来表示登录用户的全部信息;
(4)在MyUserDetailsService中实现用户权限的赋值
在MySysUserDetails类中加入两个属性,记录从数据库中查处的角色和权限信息

这里就简单一点,不在做多表关联查询了。直接把zhangsan用户设置为超级管理员,拥有所有权限;lisi用户设置为普通管理员,拥有基本权限。
在MyUserDetailsService中实现用户权限的赋值:
@Component
public class MyUserDetailsService implements UserDetailsService {
/*
* UserDetailsService:提供查询用户功能,如根据用户名查询用户,并返回UserDetails
*UserDetails,SpringSecurity定义的类, 记录用户信息,如用户名、密码、权限等
* */
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Autowired
private SysPermissionsMapper sysPermissionsMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库中查询用户
SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(username != null, SysUser::getUsername, username));
if (sysUser==null){
throw new UsernameNotFoundException("用户不存在");
}
MySysUserDetails mySysUserDetails=new MySysUserDetails(sysUser);
if ("zhangsan".equals(username)){
//zhangsan用户是超级管理员,拥有一切权限
SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleName, "超级管理员"));
Set<SysRole> roles=new HashSet<>();
roles.add(sysRole);
mySysUserDetails.setRoles(roles);
SysPermissions sysPermissions = sysPermissionsMapper.selectById(1);
Set<String> permissions=new HashSet<>();
permissions.add(sysPermissions.getPermissionsName());
mySysUserDetails.setPermissions(permissions);
}
if ("lisi".equals(username)){
//lisi用户是普通管理员,拥有基本权限
SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleName, "普通管理员"));
Set<SysRole> roles=new HashSet<>();
roles.add(sysRole);
mySysUserDetails.setRoles(roles);
SysPermissions sysPermissions = sysPermissionsMapper.selectById(2);
Set<String> permissions=new HashSet<>();
permissions.add(sysPermissions.getPermissionsName());
mySysUserDetails.setPermissions(permissions);
}
return mySysUserDetails;
}
}
(5)MySysUserDetails中完成角色和权限的赋值
private Set<SysRole> roles;
// 权限信息
private Set<String> permissions;
// 用户拥有的权限集合,我这里先设置为null,将来会再更改的
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
System.err.println("进入权限的获取方法");
List<GrantedAuthority> authorities = new ArrayList<>(); // 授权信息列表
// 将角色名称添加到授权信息列表中
roles.forEach(role->
authorities.add(new SimpleGrantedAuthority(role.getRoleName())));
// 将权限名称添加到授权信息列表中
permissions.forEach(permission->
authorities.add(new SimpleGrantedAuthority(permission))
);
return authorities; // 返回授权信息列表
}
用户认证之后,会去存储用户对应的权限,并且给资源设置对应的权限,SpringSecurity支持两种粒度 的权限:
1、基于请求的:在配置文件中配置路径,可以使用**的通配符
2、基于方法的:在方法上使用注解实现
角色配置:在UserDetails接口中存在相关的权限和角色管理,只不过我们在实现这个接口的时候,将这些都设置为了null。现在我们只需要将这些信息实现即可
(6)基于请求
还是在SecurityFilter过滤器中实现请求地址的权限校验
httpSecurity.authorizeHttpRequests(it->
//hello地址只有超级管理员角色才能访问
it.requestMatchers("/hello").hasRole("超级管理员")
//hello2地址只有"拥有所有权限"的权限才能访问
.requestMatchers("hello2").hasAuthority("拥有所有权限")
.requestMatchers("/login","sys-user/login").permitAll() //设置登录路径所有人都可以访问
.anyRequest().authenticated() //其他路径都要进行拦截
);
使用sili进行登录时,访问hello2接口显示权限不够:

使用zhangsan进行登录时,访问hello2接口可以访问到:

(7)基于方法
基于方法的权限认证要在SecurityConfig类上加上@EnableMethodSecurity注解,表示开启了方法权限的使用;
常用的有四个注解:
@PreAuthorize
@PostAuthorize
@PreFilter
@PostFilter
/*测试@PreAuthorize注解
* 作用:使用在类或方法上,拥有指定的权限才能访问(在方法运行前进行校验)
* String类型的参数:语法是Spring的EL表达式
* 有权限:test3权限
* hasRole:会去匹配authorities,但是会在hasRole的参数前加上一个ROLE_前缀,
* 所以在定义权限的时候需要加上ROLE_前缀
* role和authorities的关系是:role是一种复杂的写法,有ROLE_前缀,authorities是role的简化写法
* 如果使用
* hasAnyRole:则匹配的权限是在authorities加上前缀ROLE_
* 推荐使用
* hasAnyAuthority:匹配authorities,但是不用在authorities的参数前加上ROLE_前缀
* */
@PreAuthorize("hasAnyAuthority('拥有所有权限')")
@ResponseBody
@GetMapping("/test3")
public String test3(){
System.out.println("一个请求");
return "一个test3请求";
}
/*
@PostAuthorize:在方法返回时进行校验。
可以还是校验权限、或者校验一些其他的东西(接下来我们校验返回值的长度)
*返回结果的长度大于3、则认为是合法的
returnObject:固定写法,代指返回对象
* */
@ResponseBody
@PostAuthorize("returnObject.length()>4")
@GetMapping("/test4")
public String test4(){
System.out.println("一个test4请求");
return "小张自傲张最终";
}
/*
* @PreFilter:过滤符合条件的数据进入到接口
* */
@PostFilter("filterObject.length()>3")
@ResponseBody
@GetMapping("/test5")
public String test5(){
System.out.println("一个test4请求");
List<String> list = new ArrayList<>();
list.add("张三");
list.add("王麻子");
list.add("狗叫什么");
return "一个test5请求";
}
/*
* @PreFilter:过滤符合条件的数据返回,数据必须是Collection、map、Array【数组】
* */
@PreFilter("filterObject.length()>5")
@ResponseBody
@PostMapping("/test6")
public List<String> test6(@RequestBody List<String> list){
return list;
}
需要注意的是这些方法不仅仅局限在权限的校验,还能对返回的结果做一定的操作;
最需要注意的就是@PreFilter注解,它要求前端传递的参数一定是数组或集合;
基于方法鉴权 在SpringSecurity6版本中@EnableGlobalMethodSecurity被弃用,取而代之的是 @EnableMethodSecurity。默认情况下,会激活pre-post注解,并在内部使用 AuthorizationManager。
新老API区别 此@EnableMethodSecurity替代了@EnableGlobalMethodSecurity。提供了以下改进:
- 使用简化的AuthorizationManager。
- 支持直接基于bean的配置,而不需要扩展GlobalMethodSecurityConfiguration
- 使用Spring AOP构建,删除抽象并允许您使用Spring AOP构建块进行自定义
- 检查是否存在冲突的注释,以确保明确的安全配置
- 符合JSR-250
- 默认情况下启用@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter

【3】总结
(1)登录校验(Authentication):
1-用户提交用户名和密码进行登录。
2-Spring Security会拦截登录请求,并将用户名和密码与存储在系统中的凭据(如数据库或LDAP)进行比对。
3-如果用户名和密码匹配,则认为用户通过了身份验证,可以继续访问受限资源。
4-认证成功后,Spring Security会创建一个包含用户信息和权限的安全上下文(Security Context)。
(2)权限认证(Authorization):
1-一旦用户通过了身份验证,Spring Security就会开始进行权限认证。
2-针对每个受限资源或操作,可以配置相应的权限要求,例如需要哪些角色或权限才能访问。
3-Spring Security会根据配置的权限要求,检查当前用户所拥有的角色和权限,判断是否满足访问条件。
4-如果用户拥有足够的角色或权限,就被允许访问资源;否则将被拒绝访问,并可能重定向到登录页面或返回相应的错误信息。
Spring Security通过身份验证(Authentication)来确认用户的身份,并通过授权(Authorization)来控制用户对受保护资源的访问。这种分离的设计使得安全配置更加灵活,并且可以轻松地对不同的用户和角色进行管理和控制。
【六】微服务认证与授权实现思路
【1】说明
(1)如果是基于Session,那么SpringSecurity会对cookie里的SessionID进行解析,找到服务器存储的Session信息,然后判断当前用户是否复合请求的要求
(2)如果是token,则是解析出token,然后将当前请求加入到SpringSecurity管理的权限信息中去

如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,SpringSecurity解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样SpringSecurity就能够判断当前请求是否有权限访问。
【2】单点登录实现思路(跳转)
【七】微服务代码实例
【1】说明
搭建一个单独的认证的服务,注册到Nacos,使用ddd分层
springboot2.7.7
SpringCloud2021.0.5
有三个服务:gateway网关服务、auth认证服务、test业务服务
流程:
(1)login接口到达gateway服务,设置login接口不拦截,直接转发到auth服务,校验后使用jwt生成token返回
(2)拿到token后,调用test的接口,首先在gateway被jwt的全局过滤器拦截校验token,token通过后做路由转发到test服务,实现接口的调用
(3)test服务本身也可以对jwt再做一次校验
(4)总结,通过auth认证服务生成的token,可以用于所有服务的接口调用,实现单点登录
【2】准备工作
(1)依赖
选择自己需要的依赖导入,重点是版本要兼容
<properties>
<allenStudy.version>1.0-SNAPSHOT</allenStudy.version>
<java.version>1.8</java.version>
<camunda.version>7.17.0</camunda.version>
<mp.version>3.5.6</mp.version>
<hutool.version>5.8.18</hutool.version>
<modelmapper.version>3.1.1</modelmapper.version>
<slf4j.version>1.7.36</slf4j.version>
<freemarker.version>2.3.30</freemarker.version>
<swagger.version>2.2.15</swagger.version>
<springdoc.version>1.7.0</springdoc.version>
<druid.version>1.2.23</druid.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
<jedis.version>3.3.0</jedis.version>
<redisson.version>3.6.5</redisson.version>
</properties>
<!-- 依赖管理:统一版本 -->
<dependencyManagement>
<dependencies>
<!--注意检查mapper注解是不是mapstruct包下的,可能会错误的引入mybatis包-->
<!--mapstruct-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!--注解处理器-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<!--SpringCloud Alibaba 微服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 公共依赖(所有子模块继承) -->
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>${camunda.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>${camunda.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mp.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- spring-boot-starter-mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- ModelMapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${modelmapper.version}</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!--mbatis-plus代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mp.version}</version>
</dependency>
<!--freemarker-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<!--Swagger2-->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-models</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 使用 Jackson 进行对象序列化和反序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!--Jackson 进行 JSON 序列化时,默认情况下它不支持 Java 8 的日期时间类型(如 java.time.LocalDateTime)-->
<!--需要添加 jackson-datatype-jsr310 模块来处理这些类型-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Jedis客户端依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- EasyExcel 核心依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!--手动下载datax,安装lib下的datax-common和datax-core的jar到本地maven仓库-->
<!--mvn install:install-file -DgroupId=com.datax -DartifactId=datax-core -Dversion=1.0.0 -Dpackaging=jar -Dfile=datax-core-0.0.1-SNAPSHOT.jar-->
<dependency>
<groupId>com.datax</groupId>
<artifactId>datax-core</artifactId>
<version>1.0.0</version>
</dependency>
<!--mvn install:install-file -DgroupId=com.datax -DartifactId=datax-common -Dversion=1.0.0 -Dpackaging=jar -Dfile=datax-common-0.0.1-SNAPSHOT.jar-->
<dependency>
<groupId>com.datax</groupId>
<artifactId>datax-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
<!--actuator监控-->
<!--<dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-actuator</artifactId>-->
<!--</dependency>-->
<!-- SpringBoot Admin -->
<!--Spring Boot 应用程序运行状态监控和管理的后台界面,包含actuator包-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.6.10</version>
</dependency>
<!-- SpringBoot Admin Client -->
<!--<dependency>-->
<!-- <groupId>de.codecentric</groupId>-->
<!-- <artifactId>spring-boot-admin-starter-client</artifactId>-->
<!-- <version>2.6.10</version>-->
<!--</dependency>-->
<!--kafka-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.7</version>
</dependency>
<!---->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Nacos Discovery Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos Config Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--从2021.0.5版本起,Spring Cloud将不再默认启用bootstrap 包,需要手动引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--自己封装的common通用类-->
<dependency>
<groupId>com.allen.study</groupId>
<artifactId>test_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.6.10</version>
</dependency>
</dependencies>
(2)yml配置
bootstrap.yml
server:
port: 8091 # 指定端口号
spring:
main:
## 允许循环依赖
allow-circular-references: true
## 应用配置
application:
## 应用名称
name: allen-auth
boot:
admin:
client:
url: http://localhost:8091
profiles:
active: dev
# active: uat1 # 通过spring.profiles.active 配置多份不同环境的配置文件
# cloud 配置
cloud:
nacos:
config:
file-extension: yml ## 配置文件的后缀
# shared-configs: ## 共享配置,通过【命名空间+分组+配置集】确定一个配置文件
# - data-id: global-common.yml ## 配置集,即配置文件名称
# group: global ## 分组为group
# refresh: true ## 热刷新
# - data-id: global-mybatisplus.yml
# group: global
# refresh: true
# - data-id: global-config.yml
# group: global
# refresh: true
prefix: application
# admin监控开放全部端点
management:
endpoints:
web:
exposure:
include: "*"
# admin监控日志
logging:
file:
name: logs/${spring.application.name}/info.log
bootstrap-dev.yml
spring:
## cloud 配置
cloud:
## nacos 配置
nacos:
## nacos 注册中心配置
config:
## 服务器地址
server-addr: localhost:8848
## 命名空间ID
namespace: b3c27fd8-2f44-40d1-985a-9d743fa317b7
username: nacos #登录名
password: nacos #登录密码
group: DEFAULT_GROUP
discovery:
server-addr: localhost:8848
namespace: b3c27fd8-2f44-40d1-985a-9d743fa317b7
username: nacos #登录名
password: nacos #登录密码
group: DEFAULT_GROUP
nacos里配置了mysql和redis等信息,这里并不规范,按需要配置自己的信息
(3)启动类
用来确认bootstrap.yml配置读取成功,因为bootstrap依赖要手动导入,否则bootstrap.yml里的配置信息不会生效
package com.allen.study;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
/**
* 应用启动类
*
* @author AllenSun
* @since 2025-03-15 21:29
*/
@SpringBootApplication
@Log4j2
public class AuthApplication {
/**
* 启动项
*
* @param args
*/
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AuthApplication.class, args);
Environment env = context.getEnvironment();
// 获取端口号(默认值设置为空字符串避免空指针)
String port = env.getProperty("server.port", "未配置端口");
String contextPath = env.getProperty("server.servlet.context-path", "");
// 打印启动信息
System.out.println("\n=================================");
System.out.println("应用启动成功!访问地址: http://localhost:" + port + contextPath);
System.out.println("=================================");
}
}
(4)常用常量
package com.allen.study.test_common.constant;
/**
* 缓存的key 常量
*
* @author ruoyi
*/
public class CacheConstants
{
/**
* 缓存有效期,默认720(分钟)
*/
public final static long EXPIRATION = 720;
/**
* 缓存刷新时间,默认120(分钟)
*/
public final static long REFRESH_TIME = 120;
/**
* 密码最大错误次数
*/
public final static int PASSWORD_MAX_RETRY_COUNT = 5;
/**
* 密码锁定时间,默认10(分钟)
*/
public final static long PASSWORD_LOCK_TIME = 10;
/**
* 权限缓存前缀
*/
public final static String LOGIN_TOKEN_KEY = "login_tokens:";
/**
* 验证码 redis key
*/
public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
/**
* 参数管理 cache key
*/
public static final String SYS_CONFIG_KEY = "sys_config:";
/**
* 字典管理 cache key
*/
public static final String SYS_DICT_KEY = "sys_dict:";
/**
* 防重提交 redis key
*/
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 限流 redis key
*/
public static final String RATE_LIMIT_KEY = "rate_limit:";
/**
* 登录账户密码错误次数 redis key
*/
public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
/**
* 登录失败账户锁
*/
public static final String USER_LOGIN_LOCK = "login_locks:";
}
(5)SpringSecurity的工具类
结合redis来存登录失败的次数,如果失败超过5次就上锁
package com.allen.study.domain.utils.security;
import com.allen.study.test_common.constant.CacheConstants;
import com.allen.study.test_common.utils.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @ClassName: SecurityUtil
* @Author: AllenSun
* @Date: 2025/3/18 21:38
*/
@Component
@Slf4j
public class SecurityUtil {
@Autowired
private RedisUtils redisUtils;
private int maxRetryCount = CacheConstants.PASSWORD_MAX_RETRY_COUNT;
private Long lockTime = CacheConstants.PASSWORD_LOCK_TIME;
// 失败次数递增(原子操作)
public int incrementFailureCount(String username) {
String retryCountKey = CacheConstants.PWD_ERR_CNT_KEY + username;
redisUtils.incr(retryCountKey,1);
redisUtils.expire(retryCountKey, 10*60); // 失败记录保留10分钟
Integer retryCount = (Integer)redisUtils.get(retryCountKey);
Integer maxRetryCountVal = Integer.valueOf(maxRetryCount).intValue();
if (retryCount >= maxRetryCountVal) {
lockAccount(username);
}
return maxRetryCountVal-retryCount;
}
// 检查是否锁定
public boolean isAccountLocked(String username) {
String lockKey = CacheConstants.USER_LOGIN_LOCK + username;
Long expireTime = redisUtils.getExpire(lockKey);
return expireTime != null && expireTime > 0;
}
public long getLockLeftTime(String username) {
String lockKey = CacheConstants.USER_LOGIN_LOCK + username;
Long expireTime = redisUtils.getExpire(lockKey);
return expireTime;
}
// 执行账户锁定
public void lockAccount(String username) {
String lockKey = CacheConstants.USER_LOGIN_LOCK + username;
Long lockTime = 5*60l;
redisUtils.set(lockKey, System.currentTimeMillis());
redisUtils.expire(lockKey, lockTime); // 锁定5分钟
clearFailureCount(username);
String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, 5);
log.info(errMsg);
}
public void clearFailureCount(String username) {
String retryCountKey = CacheConstants.PWD_ERR_CNT_KEY + username;
redisUtils.del(retryCountKey);
}
}
(6)创建用户数据表
存储用户信息和加密后的密码
-- allen_camunda.employee_login definition
CREATE TABLE `employee_login` (
`id` varchar(100) NOT NULL COMMENT 'id',
`employee_id` varchar(100) NOT NULL COMMENT '员工id',
`employee_name` varchar(255) NOT NULL COMMENT '员工姓名',
`encrypted_password` varchar(255) NOT NULL COMMENT '登录密码',
`CRT_DT_TM` datetime DEFAULT NULL COMMENT '创建时间',
`CRT_USER_ID` varchar(100) DEFAULT NULL COMMENT '创建人',
`UPD_DT_TM` datetime DEFAULT NULL COMMENT '更新时间',
`UPD_USER_ID` varchar(100) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户登录信息表';
(7)生成基础代码
使用MybatisPlus相关的工具,生成基础的增删改查方法。额外添加一个根据employeeId查询用户信息的接口,用来登录的时候查询用户信息
(8)jwt的工具类
主要包括以下功能
(1)生成token
(2)解析token
(3)判断token是否已过期
(4)
package com.allen.study.common.utils.jwt;
import cn.hutool.core.convert.Convert;
import com.allen.study.common.constant.SecurityConstants;
import com.allen.study.common.exception.CustomRuntimeException;
import com.allen.study.common.utils.redis.RedisUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.function.Function;
/**
* @ClassName: JwtUtils
* @Author: AllenSun
* @Date: 2025/3/7 20:35
*
* 全局异常处理器(通常使用 @ControllerAdvice 和 @ExceptionHandler 注解)主要处理控制器方法调用过程中抛出的异常。
* 要是 JWT 异常在过滤器或者拦截器中抛出,且这些组件不在控制器方法调用链里,全局异常处理器就无法捕获这些异常。
*/
@Component
@Slf4j
public class JwtUtils {
// 秘钥
private static final String SECRET_KEY = "123456654321"; // 建议存储于环境变量
// 过期时间
private static final long TOKEN_EXPIRATION_MS = 86400000; // 24小时
// redis缓存key
private static final String TOKEN_KEY_PREFIX = "auth:token:";
@Autowired
private RedisUtils redisUtils;
// 从token中提取用户名
public String extractUsername(String token) {
// 从token中提取声明,并返回用户名
return extractClaim(token, Claims::getSubject);
}
/**
* 判断 Token 是否过期
* @param token JWT Token
* @return 如果过期返回 true,否则返回 false
*/
public boolean isTokenExpired2(String token) {
try {
Claims claims = parseToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 解析 JWT Token 并获取 Claims
* @param token JWT Token
* @return Claims 对象
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
// 从token中提取过期时间
public Date extractExpiration(String token) {
// 调用extractClaim方法,传入token和Claims::getExpiration方法引用
return extractClaim(token, Claims::getExpiration);
}
// 从token中提取指定类型的声明
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
// 从token中提取所有的claims
final Claims claims = extractAllClaims(token);
// 使用claimsResolver函数处理claims,并返回结果
return claimsResolver.apply(claims);
}
// 从token中提取所有声明
public Claims extractAllClaims(String token) {
// 使用SECRET_KEY解析token,并返回声明
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
} catch (SignatureException e) {
log.info("无效的JWT签名:{}",e.getMessage());
throw new CustomRuntimeException("无效的JWT签名,请重新登录");
} catch (MalformedJwtException e) {
log.info("无效的JWT签名:{}",e.getMessage());
throw new CustomRuntimeException("无效的JWT签名,请重新登录");
}
return claims;
}
// 判断token是否过期
private Boolean isTokenExpired(String token) {
// 提取token中的过期时间
return extractExpiration(token).before(new Date());
}
/**
* 根据令牌获取用户标识
*
* @param token 令牌
* @return 用户ID
*/
public String getUserKey(String token)
{
Claims claims = extractAllClaims(token);
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据身份信息获取键值
*
* @param claims 身份信息
* @param key 键
* @return 值
*/
public String getValue(Claims claims, String key)
{
return Convert.toStr(claims.get(key), "");
}
/**
* 根据令牌获取用户标识
*
* @param claims 身份信息
* @return 用户ID
*/
public String getUserKey(Claims claims)
{
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据身份信息获取用户ID
*
* @param claims 身份信息
* @return 用户ID
*/
public String getUserId(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USER_ID);
}
/**
* 根据令牌获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUserName(String token)
{
Claims claims = extractAllClaims(token);
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
/**
* 根据身份信息获取用户名
*
* @param claims 身份信息
* @return 用户名
*/
public String getUserName(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
}
【3】整合SpringSecurity实现auth认证服务的登录
(1)实现UserDetails接口,封装用户信息用来传递登录信息到过滤器链路
package com.allen.study.domain.utils.security;
import com.allen.study.domain.entity.EmployeeLogin;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @ClassName: MySysUserDetails
* @Author: AllenSun
* @Date: 2025/3/6 22:13
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MySysUserDetails implements UserDetails {
private Integer id;
private String username;
private String password;
private String token;
// 用户拥有的权限集合,我这里先设置为null,将来会再更改的
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public MySysUserDetails(EmployeeLogin employeeLogin) {
// this.id = employeeLogin.getId();
this.username = employeeLogin.getEmployeeId();
this.password = employeeLogin.getEncryptedPassword();
this.token = employeeLogin.getToken();
}
// 后面四个方法都是用户是否可用、是否过期之类的。我都设置为true
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
(2)实现UserDetailsService接口,自定义获取用户信息的方法
这里就会用到自己的接口方法:根据employeeId查询用户信息,然后封装到自定义的MySysUserDetails
package com.allen.study.domain.utils.security;
import com.allen.study.domain.entity.EmployeeLogin;
import com.allen.study.domain.repository.IEmployeeLoginRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
/**
* @ClassName: MyUserDetailsService
* @Author: AllenSun
* @Date: 2025/3/6 22:09
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
/*
* UserDetailsService:提供查询用户功能,如根据用户名查询用户,并返回UserDetails
* UserDetails,SpringSecurity定义的类, 记录用户信息,如用户名、密码、权限等
* */
@Autowired
private IEmployeeLoginRepo employeeLoginRepo;
@Override
public UserDetails loadUserByUsername(String employeeId) throws UsernameNotFoundException {
//根据用户名从数据库中查询用户
EmployeeLogin employeeLogin = employeeLoginRepo.queryByEmployeeId(employeeId);
if (employeeLogin==null){
throw new UsernameNotFoundException("用户不存在");
}
// 封装查询到的用户信息
MySysUserDetails mySysUserDetails=new MySysUserDetails(employeeLogin);
return mySysUserDetails;
}
}
(3)创建自定义的JWT过滤器
package com.allen.study.domain.utils.security;
import com.allen.study.test_common.exception.CustomRuntimeException;
import com.allen.study.test_common.utils.jwt.JwtUtils;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName: JwtAuthenticationFilter
* @Author: AllenSun
* @Date: 2025/3/7 20:39
*
* 从请求头中提取 JWT,验证其有效性,并将用户信息设置到 Spring Security 的上下文当中。
*
*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 注入 JwtUtils
@Autowired
private JwtUtils jwtUtils;
// 注入 UserDetailsService
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取请求头中的 Authorization 字段
log.info("JWT过滤器:开始:{}",request.getRequestURL());
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 如果 Authorization 字段不为空且以 Bearer 开头
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// 获取 JWT
jwt = authorizationHeader.substring(7);
try {
// 从 JWT 中提取用户名
username = jwtUtils.extractUsername(jwt);
log.info("JWT过滤器:解析token得到用户名:{}",username);
} catch (ExpiredJwtException e) {
// 处理 JWT 过期的情况
log.info("JWT过滤器:JWT 已过期,请重新登录");
throw new CustomRuntimeException("JWT 已过期,请重新登录");
}
}
// 如果用户名不为空且 SecurityContextHolder 中没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 从 UserDetailsService 中加载用户信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证 JWT 是否有效
if (jwtUtils.validateToken(jwt, userDetails)) {
// 创建 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 设置认证信息
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将认证信息放入 SecurityContextHolder 中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
log.info("JWT过滤器:成功:{}",request.getRequestURL());
// 继续执行过滤器链
filterChain.doFilter(request, response);
}
}
(4)创建SecurityConfig,将上述配置加载
package com.allen.study.domain.utils.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @ClassName: SecurityConfig
* @Author: AllenSun
* @Date: 2025/3/6 22:16
*/
@Configuration
//开启webSecurity服务
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private MyUserDetailsService myUserDetailsService;
// 验证 JWT 并进行身份验证。
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// Admin Server 需要访问静态资源
return (web) -> web.ignoring().antMatchers("http://localhost:8091/**");
}
/*
* 配置权限相关的配置
* 安全框架本质上是一堆的过滤器,称之为过滤器链,每一个过滤器链的功能都不同
* 设置一些链接不要拦截
* */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable() // 关闭csrf
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 插入JWT过滤器
.authorizeRequests(auth -> auth
// 放行 Swagger 相关路径
.antMatchers(
"/auth/login",//路径允许所有用户访问,用于用户登录获取 JWT。
"/employeelogin/**",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/swagger-resources/configuration/ui",
"/swagger-resources/**",
"/swagger-resources/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/actuator/**",
"/instances/**",
"/assets/**",
"/applications/**",
"/**"
).permitAll()
// .antMatchers("/login").hasRole("ADMIN")
.anyRequest()
.authenticated()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不使用会话管理,因为 JWT 是无状态的。
;
//关闭csrf
// httpSecurity.csrf(it->it.disable());
// httpSecurity.authorizeHttpRequests(it->
// it.requestMatchers("/4.2.0/employeelogin/login").permitAll() //设置登录路径所有人都可以访问
// .anyRequest().authenticated() //其他路径都要进行拦截
// );
//表单
// httpSecurity.formLogin(from->
// from.loginPage("/login") //跳转到自定义的登录页面
// .loginProcessingUrl("/login") //处理前端的请求,与from表单的action一致即可
// .defaultSuccessUrl("/index") //默认的请求成功之后的跳转页面,直接访问登录页面
// );
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){
DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
//将编写的UserDetailsService注入进来
provider.setUserDetailsService(myUserDetailsService);
//将使用的密码编译器加入进来
provider.setPasswordEncoder(passwordEncoder);
//将provider放置到AuthenticationManager 中
ProviderManager providerManager=new ProviderManager(provider);
return providerManager;
}
/*
* 在security安全框架中,提供了若干密码解析器实现类型。
* 其中BCryptPasswordEncoder 叫强散列加密。可以保证相同的明文,多次加密后,
* 密码有相同的散列数据,而不是相同的结果。
* 匹配时,是基于相同的散列数据做的匹配。
* Spring Security 推荐使用 BCryptPasswordEncoder 作为密码加密和解析器。
* */
@Bean
public PasswordEncoder passwordEncoder(){
// return new CustomBCryptPasswordEncoder();
return new BCryptPasswordEncoder();
// return NoOpPasswordEncoder.getInstance(); // 明文处理,不做加密
}
// @Bean
// public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
// return new CustomAuthenticationSuccessHandler();
// }
//
// @Bean
// public AuthenticationFailureHandler customAuthenticationFailureHandler() {
// return new CustomAuthenticationFailureHandler();
// }
}
(5)实现登录逻辑
请求参数为employeeId和password
/**
* 用户登录信息表资源库
*/
private final IEmployeeLoginRepo employeeLoginRepo;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private RedisUtils redisUtils;
@Autowired
private SecurityUtil securityUtil;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private final static long expireTime = CacheConstants.EXPIRATION;
private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
public DomainResponse<String> login(@NotNull EmployeeLogin employeeLogin) {
Object principal = null;
String employeeId = employeeLogin.getEmployeeId();
if (securityUtil.isAccountLocked(employeeId)) {
long lockLeftTime = securityUtil.getLockLeftTime(employeeId);
String accountLockMsg = StrUtil.format("账户被锁定:{},剩余时间:{}秒",employeeId,lockLeftTime);
log.info(accountLockMsg);
throw new CustomRuntimeException(accountLockMsg);
}
try {
// 传入用户名和密码
UsernamePasswordAuthenticationToken usernamePassword =
new UsernamePasswordAuthenticationToken(employeeId,employeeLogin.getEncryptedPassword());
//是实现登录逻辑,此时就会去调用LoadUserByUsername方法
Authentication authenticate = authenticationManager.authenticate(usernamePassword);
//获取返回的用户信息
principal = authenticate.getPrincipal();
log.info("认证成功:{}",employeeId);
securityUtil.clearFailureCount(employeeId);
} catch (AuthenticationException e) {
int leftRetryCount = securityUtil.incrementFailureCount(employeeId);
String failLoginMsg = StrUtil.format("认证失败:{},剩余次数:{}",employeeId,leftRetryCount);
log.info(failLoginMsg);
throw new CustomRuntimeException(failLoginMsg);
}
//强转为MySysUserDetails类型
MySysUserDetails mySysUserDetails = (MySysUserDetails) principal;
//输出用户信息
log.info("用户信息:{}", mySysUserDetails);
//返回token
EmployeeLogin loginUser = new EmployeeLogin();
String token = IdUtils.fastUUID();
loginUser.setEmployeeId(mySysUserDetails.getUsername());
loginUser.setToken(token);
loginUser.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
//将用户信息刷新到缓存redis中。有了token令牌
refreshToken(loginUser);
// Jwt存储信息(根据jwt的token可以获取uuid-token,然后从redis获取登录信息)
mySysUserDetails.setToken(token);
String jwtToken = jwtUtils.generateToken(mySysUserDetails);
return DomainResponse.ok(jwtToken);
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(EmployeeLogin loginUser) {
// 添加登录时间
loginUser.setLoginTime(System.currentTimeMillis());
// 添加登录过期时间
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 取出uuid(token值)作为缓存的key值
String userKey = ACCESS_TOKEN + loginUser.getToken();
log.info("token的缓存key生成成功:{}",userKey);
// 把用户登录信息类loginUser作为value,uuid值作为key,存储到redis缓存中,并且设置了过期时间
redisUtils.set(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
login接口不会被过滤器拦截

(6)获取token后使用

【4】Gateway项目里添加token的全局过滤器
请求的时候接口先到Gateway网关,通过jwt校验过滤器,才能到达路由转发请求到其他服务
package com.allen.study.filter;
import cn.hutool.core.util.StrUtil;
import com.allen.study.common.constant.CacheConstants;
import com.allen.study.common.constant.SecurityConstants;
import com.allen.study.common.constant.TokenConstants;
import com.allen.study.common.exception.CustomRuntimeException;
import com.allen.study.common.utils.StringUtils;
import com.allen.study.common.utils.jwt.JwtUtils;
import com.allen.study.common.utils.redis.RedisUtils;
import com.allen.study.properties.IgnoreWhiteProperties;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @ClassName: JwtAuthFilter
* @Author: AllenSun
* @Date: 2025/3/15 22:48
*/
@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter, Ordered {
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisUtils redisUtils;
@Autowired
private JwtUtils jwtUtils;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
log.info("开始认证token:{}",url);
// 跳过白名单不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
log.info("白名单路径,结束认证:{}",url);
return chain.filter(exchange);
}
String token = getToken(request);
// token令牌为空
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
// Jwt
Claims claims = jwtUtils.extractAllClaims(token);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
// 判断是否过期
if (jwtUtils.isTokenExpired2(token)) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
// userid或者username为空的
// String userid = jwtUtils.getUserId(claims);
String username = claims.getSubject();
if (StringUtils.isEmpty(username)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
log.info("完成认证token:{}",url);
// 设置用户信息到请求
// addHeader(mutate, SecurityConstants.USER_KEY, userkey);
// addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
if (value == null) {
return;
}
String valueStr = value.toString();
// String valueEncode = ServletUtils.urlEncode(valueStr);
String valueEncode = null;
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
String failAuthMsg = StrUtil.format("鉴权异常:{},请求路径:{}", msg, exchange.getRequest().getPath());
log.error(failAuthMsg);
// return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
throw new CustomRuntimeException(failAuthMsg);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
@Override
public int getOrder()
{
return -200;
}
}
【5】服务间使用Openfeign接口时传递token
package com.allen.study.common.feign.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName: FeignInterceptorConfig
* @Author: AllenSun
* @Date: 2025/3/20 12:53
*/
@Configuration
public class FeignInterceptorConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 拦截器可用于在请求发送前或响应接收后执行一些额外的逻辑,例如添加请求头。
// template.header("Authorization", "Bearer your_token");
// 从请求上下文中获取当前的 HttpServletRequest
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 从请求头中获取 JWT Token
String jwtToken = request.getHeader("Authorization");
if (jwtToken != null) {
// 将 JWT Token 添加到 Feign 请求头中
template.header("Authorization", jwtToken);
}
}
}
};
}
}

8870

被折叠的 条评论
为什么被折叠?



