Spring Security + JWT + 国密 SM4 实现用户认证&授权

芋道源码 2024年07月09日 09:31 上海

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7250357906713215037


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

  • 基础框架: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),是目前比较流行的用户身份验证解决方案。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

JWT生成和认证的基本流程

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

图片

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

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 测试登录接口。

认证成功返回结果截图:

图片

认证失败返回结果截图:

图片


欢迎加入我的知识星球,全面提升技术能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security JWT实现是指使用JWT(JSON Web Token)作为身份验证和授权机制的Spring Security解决方案。Spring SecurityJWT提供了自动化配置,使得使用JWT进行身份验证和授权变得更加简单和高效。通过配置JwtAuthenticationTokenFilter,可以实现JWT的验证和解析。同时,可以通过RestfulAccessDeniedHandler和RestAuthenticationEntryPoint来处理登录校验和权限校验的逻辑。使用JWT实现Spring Security解决方案可以提供更加强大和灵活的身份验证和授权功能。 [2 [3<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [厉害,我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证](https://blog.csdn.net/qing_gee/article/details/124016059)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [单点登录SSO解决方案之SpringSecurity+JWT实现.docx](https://download.csdn.net/download/njbaige/34581331)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值