【SpringSecurity】springboot整合SpringSecurity实现登录校验与权限认证

【SpringSecurity】springboot整合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。提供了以下改进:

  1. 使用简化的AuthorizationManager。
  2. 支持直接基于bean的配置,而不需要扩展GlobalMethodSecurityConfiguration
  3. 使用Spring AOP构建,删除抽象并允许您使用Spring AOP构建块进行自定义
  4. 检查是否存在冲突的注释,以确保明确的安全配置
  5. 符合JSR-250
  6. 默认情况下启用@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);
                    }
                }
            }


        };
    }
}

【6】JWT相关的知识详解(跳转)

JWT相关的知识详解

《Vue和SpringBoot打造假日旅社管理系统》课程,将讲解如何使用Vue和SpringBoot开发这个项目,手把手演示开发流程!附赠源码、文档、数据库脚本等全部资料,提供售后答疑。 课程简介本课程讲解的是《基于 Vue 和 SpringBoot 的假日旅社管理系统》,该系统支持民宿档案、民宿新闻、民宿预定、民宿评论这四大核心业务,适用于乡村民宿企业的民宿预定业务。系统给每个民宿档案提供一个唯一标识,对民宿预定、评论等各个环节进行快速批量的数据采集,确保游客及时掌握景区民宿的真实数据,方便游客进行民宿预定。另外系统还包括员工管理、组织机构管理、文件管理、权限管理功能,给旅社企业提供更个性化的民宿管理模式。假日旅社管理系统采用了基于角色的访问控制,角色和菜单关联,一个角色可以配置多个菜单权限;然后再将用户和角色关联,一位用户可以赋予多个角色。这样用户就可以根据角色拿到该有的菜单权限,更方便旅社企业的管理人员进行权限管控。   软件技术选型前端Vue:Vue 是构建前端界面的核心框架,本系统采用 2.6.14 版本。View UI:基于 Vue.js2.0 的组件库,本系统采用 4.7.0 版本。后端Spring Boot:构建系统核心逻辑的后端框架,本系统采用 2.7.0 版本。MyBatis / MyBatis Plus:后端连接数据库的框架,本系统采用 3.5.2 版本。数据库MySQL:本项目的主数据库,本系统采用 8.0.29 版本。Redis:本系统采用基于 Windows 版本的 Redis,用于图形验证码和用户菜单权限的临时存储,采用了 5.0.14 版本。开发环境VsCode:项目前端的开发工具,使用版本为 1.68.0。IntelliJ IDEA :项目后端的开发工具,使用版本为 2021.3.2。Jdk:Java 的开发环境,使用版本为 17.0.3.1。Maven:后端项目的打包工具,使用版本为 3.6.2。NodeJs:前端项目的开发环境,使用版本为 16.13.0。 软件架构分析基于 Vue 和 SpringBoot 的假日旅社管理系统包括了系统基础模块、民宿档案模块、民宿新闻模块、民宿预定模块、民宿评论模块这五大功能模块,其架构如下图所示。  接下来,分别对五大模块进行详细介绍。系统基础模块系统基础模块,是用于支撑假日旅社管理系统的正常运行,这个模块包括了登陆注册模块、员工部门管理、菜单权限管理等。假日旅社管理系统支持用户使用账号、密码和图形验证码登陆,操作界面如下图所示。  假日旅社管理系统支持用户使用手机号、姓名、密码和图形验证码注册,操作界面如下图所示。 用户成功进入系统后,可进入到基于 Vue 和 SpringBoot 的假日旅社管理系统的首页,首页展示了当前登陆的地址、现在的时间和用户配置的常用模块,如下图所示。 接着用户点击左侧的用户管理,即可进入用户管理模块,用户管理模块的首页如下图所示。 用户可以在这个模块对系统登陆用户的档案进行维护,包括添加新用户、删除用户、编辑用户、根据姓名/部门查询用户。用户可以进入部门管理模块,管理旅社的部门数据,如下图所示。 同理用户可以进入到菜单管理模块,对系统的菜单进行管理,菜单管理模块的操作界面如下图所示。 民宿档案模块第二个模块是民宿档案模块,民宿档案就是用来管理民宿的数据,民宿档案包括民宿的名称、面积、房号、房间类型、早餐情况、价格、备注等,以下是民宿档案模块的主界面。用户可以点击顶部的“新增”按钮,进入民宿档案添加界面,添加民宿档案数据,如下图所示。 其中房间类型为下拉框单项选择,如下图所示。还有早餐情况也是下拉单选,如下图所示。 用户可以对现有的民宿档案数据进行编辑更新,只需点击每一行民宿档案数据的“编辑”按钮,即可进入民宿档案数据的编辑界面,如下图所示。 用户也可以对不需要的民宿数据进行删除操作,用户点击删除时,系统会弹出二次确认弹框,如下图所示。  民宿新闻模块第三个模块是民宿新闻模块,民宿新闻就是用来管理民宿的新闻资讯,包含的功能如下所示。 民宿新闻包括民宿的名称、面积、房号、房间类型、早餐情况、价格、备注等,以下是民宿新闻模块的主界面,其中的图片仅供测试样例使用。用户可以点击顶部的“新增”按钮,进入民宿新闻添加界面,添加民宿新闻数据,如下图所示。 新闻描述字段采用了 ueditor 富文本编辑器,这是由百度 web 前端研发部开发的所见即所得的开源富文本编辑器,具有轻量、可定制、用户体验优秀等特点,基于 MIT 开源协议,所有源代码可自由修改和使用。 用户可以对现有的民宿新闻数据进行编辑更新,只需点击每一行民宿新闻数据的“编辑”按钮,即可进入民宿新闻数据的编辑界面,如下图所示。 民宿预定模块第四个模块是民宿预定模块,旅客可以在民宿预定模块中预定民宿,达到旅客的住宿目的,民宿预定模块包含的功能如下所示。民宿预定包括了预定民宿 ID、预定民宿名称、预定日期、下单时间、下单人 ID、下单人姓名、价格、是否付款、下单备注等字段,旅客首先进入民宿档案模块,可以看到每一行民宿数据都有一个预约按钮,如下图所示。 如用户点击 1 幢 102 民宿的预约按钮后,会弹出预约确认框,需要输入预约的日期,日期表单默认选择今日,如下图所示。 旅客需要点击“确认预约”按钮,完成预约操作,系统给“预约成功”提示,如下图所示。 预约成功后,旅客可以从民宿预定模块中进行查询,如下图所示。 最后旅客进行付款操作,点击每一行右侧的付款按钮,如下图所示。支付完成后,系统将预定单的支付状态改为付款,预定流程结束,如下图所示。 民宿评论模块 第五个模块是民宿预定模块,旅客可以在民宿预定结束后评论民宿,以帮助更多的人了解民宿,民宿评论模块包含的功能如下所示。 民宿评论包括了民宿名称、民宿 ID、评论时间、评论内容、评论人 ID、评论人姓名等字段,旅客首先进入民宿档案模块,可以看到每一行民宿数据都有一个评论按钮,如下图所示。 旅客点击评论后,系统给弹框反馈,如下图所示。  用户输入评论内容后,点击确认评论按钮,即可完成评论操作,如下图所示。  旅客评论后,即可在民宿评论模块中查看此评论数据,如下图所示。 也可以在民宿模块中,双击民宿数据查看评论信息,如下图所示。 项目总结本软件是基于 Vue 和 SpringBoot 的假日旅社管理系统,包含了民宿档案、民宿新闻、民宿预定、民宿评论这四个功能模块。 开发本系统的目的,就是为了帮助旅游景点的民宿企业提高民宿管理效率,降低人力成本,让旅游景点的民宿企业获得更多的经济效益。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值