⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7250357906713215037 「汪小成」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

本示例采用的技术框架如下所示:

  • 基础框架:Spring Boot 2.7.7
  • 持久层框架:MyBatis Plus 3.5.3.1
  • 工具类库:Hutool 5.7.22
  • 缓存:Redis
  • 数据库:MySQL 8
  • 加密算法:国密SM4
  • 身份验证:JWT
  • 简化代码:lombok

JWT简介

JWT(JSON Web Token),是目前比较流行的用户身份验证解决方案。

JWT生成和认证的基本流程

下面是一个简化的时序图,用于说明JWT生成和认证的基本流程。

Spring Security简介

Spring Security is a powerful and highly customizable authentication and access-control framework.

—— 引自官网

Spring Security 是一个功能强大且高度可定制的安全框架。

引入 Spring Security 依赖

在 Spring Boot 项目中集成 Spring Security,需要在pom.xml文件中配置所需依赖。如下所示:

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

Spring Security 配置说明

注意: 从 Spring Boot 2.7.0 版本开始,Spring Security 废弃用了WebSecurityConfigurerAdapter。

在 Spring Security 配置文件中,我们通常需要做如下配置:

  • AuthenticationProvider实现类: 用于自定义身份验证逻辑;
  • Filter: 用于验证 token 有效性;
  • AuthenticationManager: 用于接收并处理身份验证请求;
  • PasswordEncoder: 用于密码加密和验证;
  • SecurityFilterChain: 过滤器链;

自定义PasswordEncoder

Spring Security的PasswordEncoder是用于进行密码加密和验证的接口。它是一个密码编码器,用于将用户的原始密码转换为安全的加密字符串,并在验证过程中将加密后的密码与用户提供的密码进行比较。

PasswordEncoder接口的主要用于提供安全的密码存储和验证机制,以防止用户密码泄露时被恶意使用。它是一种重要的安全性措施,用于保护用户密码的安全性。

Spring Security 提供了多种PasswordEncoder接口的实现类,包括:

  • BCryptPasswordEncoder: 使用BCrypt算法进行密码哈希和验证。它是目前广泛使用的密码哈希算法之一,具有较高的安全性。
  • NoOpPasswordEncoder: 不进行任何密码编码和哈希操作,即明文存储密码。不推荐在生产环境中使用,仅用于测试目的。
  • Pbkdf2PasswordEncoder: 使用PBKDF2算法进行密码哈希和验证。它通过应用哈希函数多次迭代和盐值,增加了密码破解的难度。
  • MessageDigestPasswordEncoder: 使用指定的消息摘要算法(如MD5、SHA-1、SHA-256等)进行密码哈希和验证。

使用国密(SM4)算法实现自定义的 PasswordEncoder

使用国密(SM4)算法实现自定义的 PasswordEncoder,您需要执行以下步骤:

1、添加依赖

<!-- SM4依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.71</version>
</dependency>

2、自定义的 PasswordEncoder —— Sm4PasswordEncoder.java

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.SmUtil;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.nio.charset.StandardCharsets;
import java.util.Objects;

publicclassSm4PasswordEncoderimplementsPasswordEncoder{

// key长度必须为16
privatestaticfinalStringKEY="KeyMustBe16Size.";

@Override
publicStringencode(CharSequence rawPassword){
returnSmUtil.sm4(KEY.getBytes(StandardCharsets.UTF_8)).encryptHex(rawPassword.toString());
}

@Override
publicbooleanmatches(CharSequence rawPassword, String encodedPassword){
returnObjects.equals(rawPassword.toString(),
SmUtil.sm4(KEY.getBytes(StandardCharsets.UTF_8)).decryptStr(encodedPassword,StandardCharsets.UTF_8));
}
}

需要实现PasswordEncoder接口的encode()和matches()方法。encode()方法用于对明文密码进行加密处理,matches()方法用于比较明文密码与加密后的密码是否匹配。

3、在 Spring Security 配置文件中配置自定义的 PasswordEncoder

@Configuration
@EnableWebSecurity
publicclassWebSecurityConfig{
// 其它代码
+/**
+ * 密码加密方式配置
+ */
+@Bean
+publicPasswordEncoderpasswordEncoder(){
+returnnewSm4PasswordEncoder();
+}
}

自定义 Filter 验证 token 有效性

1、实现UserDetailsService接口,用于获取用户详细信息

import cn.ddcherry.springboot.demo.entity.User;
import cn.ddcherry.springboot.demo.service.RoleService;
import cn.ddcherry.springboot.demo.service.UserService;
import cn.ddcherry.springboot.demo.security.model.AuthUser;
import cn.ddcherry.springboot.demo.util.WebUtil;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
publicclassUserDetailsServiceImplimplementsUserDetailsService{

@Resource
privateUserService userService;
@Resource
privateRoleService roleService;

@Override
publicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{
Useruser= userService.findByUsername(username);
if(Objects.isNull(user)){
thrownewUsernameNotFoundException("用户名或密码错误!");
}
List<String> roleCodeList = roleService.findRoleCodesByUsername(username);
List<GrantedAuthority> authorities = roleCodeList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
returnnewAuthUser(user.getId(), user.getRealName(), user.getAvatar(), user.getPhone(),
user.getUsername(), user.getPassword(), authorities);
}
}

UserDetailsServiceImpl类实现了UserDetailsService接口,重写了loadUserByUsername方法,用于获取用户的详细信息。

获取用户详细信息的大体流程:

其中AuthUser为自定义认证用户信息类,代码如下:

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Getter
publicclassAuthUserextendsUser{
/**
* 用户ID
*/
privatefinalString userId;
/**
* 真实姓名
*/
privatefinalString realName;
/**
* 电话
*/
privatefinalString phone;
/**
* 头像
*/
privatefinalString avatar;

publicAuthUser(String userId, String realName, String avatar, String phone, String username, String password,
Collection<? extends GrantedAuthority> authorities){
super(username, password,true,true,true,true, authorities);
this.userId = userId;
this.realName = realName;
this.avatar = avatar;
this.phone = phone;
}
}

AuthUser继续org.springframework.security.core.userdetails.User,添加了一些业务属性。

2、自定义 Filter 验证 token 有效性

import cn.ddcherry.springboot.demo.constant.AuthConstant;
import cn.ddcherry.springboot.demo.util.JwtUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
* 验证token有效性
*/
@Slf4j
publicclassTokenFilterextendsOncePerRequestFilter{

@Resource
privateUserDetailsService userDetailsService;

@Override
protectedvoiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throwsServletException,IOException{
Stringtoken= getToken(request);
if(StrUtil.isNotEmpty(token)){
// 从Token中获取username
Stringusername=JwtUtil.getUsernameFromToken(token);
// 根据username获取用户信息
UserDetailsuserDetails= userDetailsService.loadUserByUsername(username);
// 创建身份验证对象
Authenticationauthentication
=newUsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
// 设置身份验证对象
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 过滤器链
filterChain.doFilter(request, response);
}

privateStringgetToken(HttpServletRequest request){
StringbearerToken= request.getHeader("Authorization");
if(StrUtil.isNotEmpty(bearerToken)&& bearerToken.startsWith(AuthConstant.AUTHORIZATION_BEARER)){
// 去掉令牌前缀
return bearerToken.replace(AuthConstant.AUTHORIZATION_BEARER,StrUtil.EMPTY);
}
returnnull;
}
}

自定义过滤器验证 token 效率性流程图:

3、在 Spring Security 配置文件中配置自定义的自定义 Filter

@Configuration
@EnableWebSecurity
publicclassWebSecurityConfig{
// 其它代码
+@Bean
+publicTokenFiltertokenFilter(){
+returnnewTokenFilter();
+}
}

配置 AuthenticationProvider

在 Spring Security 配置文件中配置 AuthenticationProvider

@Configuration
@EnableWebSecurity
publicclassWebSecurityConfig{

// 其它代码

+@Resource
+privateUserDetailsServiceImpl userDetailsService;

+@Bean
+publicDaoAuthenticationProviderauthenticationProvider(){
+DaoAuthenticationProviderauthProvider=newDaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService);
+ authProvider.setPasswordEncoder(passwordEncoder());
+return authProvider;
+}

}

DaoAuthenticationProvider是 Spring Security 提供的一个身份验证实现类,它使用数据库中的用户详细信息和密码加密器进行身份验证。

配置 AuthenticationManager

在 Spring Security 配置文件中配置 AuthenticationManager

@Configuration
@EnableWebSecurity
publicclassWebSecurityConfig{

// 其它代码

+@Bean
+publicAuthenticationManagerauthenticationManager(AuthenticationConfiguration authConfig)throwsException{
+return authConfig.getAuthenticationManager();
+}
}

配置过滤器链

1、自定义类,处理未经身份验证或者身份验证失败的用户访问受保护资源时的行为

import cn.ddcherry.springboot.demo.api.Result;
import cn.ddcherry.springboot.demo.api.ResultCode;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
* 处理未经身份验证或者身份验证失败的用户访问受保护资源时的行为
*/
@Component
publicclassAuthenticationEntryPointImplimplementsAuthenticationEntryPoint{
@Override
publicvoidcommence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throwsIOException,ServletException{
Stringmsg=StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(Result.fail(ResultCode.UNAUTHORIZED, msg)));
}
}

2、在 Spring Security 配置文件中配置 AuthenticationManager

import cn.ddcherry.springboot.demo.security.crypto.Sm4PasswordEncoder;
import cn.ddcherry.springboot.demo.security.filter.TokenFilter;
import cn.ddcherry.springboot.demo.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
publicclassWebSecurityConfig{

@Resource
privateUserDetailsServiceImpl userDetailsService;

+@Resource
+privateAuthenticationEntryPoint authenticationEntryPoint;

@Bean
publicDaoAuthenticationProviderauthenticationProvider(){
DaoAuthenticationProviderauthProvider=newDaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

@Bean
publicTokenFiltertokenFilter(){
returnnewTokenFilter();
}

@Bean
publicAuthenticationManagerauthenticationManager(AuthenticationConfiguration authConfig)throwsException{
return authConfig.getAuthenticationManager();
}

/**
* 密码加密方式配置
*/
@Bean
publicPasswordEncoderpasswordEncoder(){
returnnewSm4PasswordEncoder();
}

+@Bean
+publicSecurityFilterChainfilterChain(HttpSecurity http)throwsException{
+// 启用跨域资源共享(CORS)支持
+ http.cors()
+.and()
+// 禁用跨站请求伪造(CSRF)保护
+.csrf().disable()
+// 配置异常处理和身份验证入口点
+.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
+.and()
+// 配置会话管理和会话创建策略:不使用会话
+.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+.and()
+// 配置请求授权规则
+.authorizeRequests().antMatchers("/api/test/**").permitAll()
+.antMatchers("/api/auth/**").permitAll()
+// 所有其他请求需要进行身份验证
+.anyRequest().authenticated();
+
+// 配置用户身份验证逻辑
+ http.authenticationProvider(authenticationProvider());
+
+// 在UsernamePasswordAuthenticationFilter过滤器之前添加TokenFilter
+ http.addFilterBefore(tokenFilter(),UsernamePasswordAuthenticationFilter.class);
+
+return http.build();
+}
}

上面的这段代码是最终的 Spring Security 配置类代码。

登录接口

LoginController 代码如下:

import cn.ddcherry.springboot.demo.api.Result;
import cn.ddcherry.springboot.demo.security.model.AuthUser;
import cn.ddcherry.springboot.demo.util.JwtUtil;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@AllArgsConstructor
@RequestMapping("/api/auth")
publicclassLoginController{

privatefinalAuthenticationManager authenticationManager;

@PostMapping("/login")
publicResult<Map<String,Object>>login(String username, String password){
UsernamePasswordAuthenticationTokenauthenticationToken=newUsernamePasswordAuthenticationToken(username, password);
Authenticationauthentication= authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
Stringtoken=JwtUtil.createToken(username,newHashMap<>());
AuthUserauthUser=(AuthUser) authentication.getPrincipal();

Map<String,Object> resultMap =newHashMap<>(16);
resultMap.put("token", token);
resultMap.put("user", authUser);

returnResult.success(resultMap);
}
}

login()方法接收两个参数:username和password,表示用户输入的用户名和密码。根据用户名、密码创建一个UsernamePasswordAuthenticationToken对象,然后调用authenticationManager.authenticate(authenticationToken)方法,使用AuthenticationManager对身份验证令牌进行身份验证,得到一个已经通过身份验证的Authentication对象。

然后调用SecurityContextHolder.getContext().setAuthentication(authentication)方法,将验证后的Authentication对象存储到SecurityContextHolder中,以便对用户进行身份认证。

调用 JWT 工具类生成 token 。调用authentication.getPrincipal()方法获取经过验证的用户信息,强制类型转换为AuthUser类型。统一放在 Map 中返回。

测试

启动项目,使用 ApiPost7 测试登录接口。

认证成功返回结果截图:

认证失败返回结果截图:

文章目录
  1. 1. JWT简介
  2. 2. JWT生成和认证的基本流程
  3. 3. Spring Security简介
  4. 4. 引入 Spring Security 依赖
  5. 5. Spring Security 配置说明
  6. 6. 自定义PasswordEncoder
    1. 6.0.1. 使用国密(SM4)算法实现自定义的 PasswordEncoder
  • 7. 自定义 Filter 验证 token 有效性
  • 8. 配置 AuthenticationProvider
  • 9. 配置 AuthenticationManager
  • 10. 配置过滤器链
  • 11. 登录接口
  • 12. 测试