本示例选用的技术结构如下所示:

  • 基础结构: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生成和认证的基本流程。

sequenceDiagram
autonumber
    participant B as 浏览器
    participant S as 服务器
    B->>S: 发送登录数据(用户名、暗码)
    S->>S: 验证用户名、暗码,生成Token
    S->>B: 将Token返给浏览器
    B->>S: 调用接口,恳求头中含有Token信息
    S->>S: 验证Token
    S->>B: 回来接口履行成果

笔者汪小成之前写的一篇介绍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:过滤器链;
Spring Boot | Spring Security + JWT + 国密SM4实现用户认证&授权

自定义PasswordEncoder

Spring Security的PasswordEncoder是用于进行暗码加密和验证的接口。它是一个暗码编码器,用于将用户的原始暗码转换为安全的加密字符串,并在验证过程中将加密后的暗码与用户供给的暗码进行比较。PasswordEncoder接口的首要用于供给安全的暗码存储和验证机制,以防止用户暗码泄露时被歹意运用。它是一种重要的安全性办法,用于维护用户暗码的安全性。

Spring Security 供给了多种PasswordEncoder接口的完成类,包括:

  1. BCryptPasswordEncoder:运用BCrypt算法进行暗码哈希和验证。它是现在广泛运用的暗码哈希算法之一,具有较高的安全性。
  2. NoOpPasswordEncoder:不进行任何暗码编码和哈希操作,即明文存储暗码。不推荐在出产环境中运用,仅用于测验意图。
  3. Pbkdf2PasswordEncoder:运用PBKDF2算法进行暗码哈希和验证。它通过应用哈希函数多次迭代和盐值,增加了暗码破解的难度。
  4. 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;
public class Sm4PasswordEncoder implements PasswordEncoder {
	// key长度必须为16
	private static final String KEY = "KeyMustBe16Size.";
	@Override
	public String encode(CharSequence rawPassword) {
		return SmUtil.sm4(KEY.getBytes(StandardCharsets.UTF_8)).encryptHex(rawPassword.toString());
	}
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		return Objects.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
public class WebSecurityConfig {
	// 其它代码
+	/**
+	 * 暗码加密办法装备
+	 */
+	@Bean
+	public PasswordEncoder passwordEncoder() {
+		return new Sm4PasswordEncoder();
+	}
}

自定义 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
public class UserDetailsServiceImpl implements UserDetailsService {
	@Resource
	private UserService userService;
	@Resource
	private RoleService roleService;
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userService.findByUsername(username);
		if (Objects.isNull(user)) {
			throw new UsernameNotFoundException("用户名或暗码过错!");
		}
		List<String> roleCodeList = roleService.findRoleCodesByUsername(username);
		List<GrantedAuthority> authorities = roleCodeList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
		return new AuthUser(user.getId(), user.getRealName(), user.getAvatar(), user.getPhone(),
			user.getUsername(), user.getPassword(), authorities);
	}
}

UserDetailsServiceImpl类完成了UserDetailsService接口,重写了loadUserByUsername办法,用于获取用户的详细信息。

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

graph TB
A[依据用户名获取用户信息] --> E{用户信息是否存在}
E --不存在--> ERROR[抛出异常] --> END 
E --存在--> ROLES[依据用户名获取角色列表] --> CONVERT[将角色编码转换成SimpleGrantedAuthority目标] --> RESULT[回来用户信息]
--> END([完毕])

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

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Getter
public class AuthUser extends User {
	/**
	 * 用户ID
	 */
	private final String userId;
	/**
	 * 实在姓名
	 */
	private final String realName;
	/**
	 * 电话
	 */
	private final String phone;
	/**
	 * 头像
	 */
	private final String avatar;
	public AuthUser(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
public class TokenFilter extends OncePerRequestFilter {
	@Resource
	private UserDetailsService userDetailsService;
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		String token = getToken(request);
		if (StrUtil.isNotEmpty(token)) {
			// 从Token中获取username
			String username = JwtUtil.getUsernameFromToken(token);
			// 依据username获取用户信息
			UserDetails userDetails = userDetailsService.loadUserByUsername(username);
			// 创立身份验证目标
			Authentication authentication
				= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
			// 设置身份验证目标
			SecurityContextHolder.getContext().setAuthentication(authentication);
		}
		// 过滤器链
		filterChain.doFilter(request, response);
	}
	private String getToken(HttpServletRequest request) {
		String bearerToken = request.getHeader("Authorization");
		if (StrUtil.isNotEmpty(bearerToken) && bearerToken.startsWith(AuthConstant.AUTHORIZATION_BEARER)) {
			// 去掉令牌前缀
			return bearerToken.replace(AuthConstant.AUTHORIZATION_BEARER, StrUtil.EMPTY);
		}
		return null;
	}
}

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

graph TD
A[从恳求头中获取token信息] --> E{token是否为空}
E --不存在--> END 
E --存在--> USERNAME[解析token信息获取username] --> USERINFO[依据username获取用户信息] --> Authentication[创立身份验证目标] --> SET[设置身份验证目标]
--> END([继续履行其它过滤器])

3、在 Spring Security 装备文件中装备自定义的自定义 Filter

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
	// 其它代码
+	@Bean
+	public TokenFilter tokenFilter() {
+		return new TokenFilter();
+	}
}

装备 AuthenticationProvider

在 Spring Security 装备文件中装备 AuthenticationProvider

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
	// 其它代码
+	@Resource
+	private UserDetailsServiceImpl userDetailsService;
+	@Bean
+	public DaoAuthenticationProvider authenticationProvider() {
+		DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+		authProvider.setUserDetailsService(userDetailsService);
+		authProvider.setPasswordEncoder(passwordEncoder());
+		return authProvider;
+	}
}

DaoAuthenticationProvider是 Spring Security 供给的一个身份验证完成类,它运用数据库中的用户详细信息和暗码加密器进行身份验证。

装备 AuthenticationManager

在 Spring Security 装备文件中装备 AuthenticationManager

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
	// 其它代码
+	@Bean
+	public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
+		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
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		String msg = 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
public class WebSecurityConfig {
	@Resource
	private UserDetailsServiceImpl userDetailsService;
+	@Resource
+	private AuthenticationEntryPoint authenticationEntryPoint;
	@Bean
	public DaoAuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
		authProvider.setUserDetailsService(userDetailsService);
		authProvider.setPasswordEncoder(passwordEncoder());
		return authProvider;
	}
	@Bean
	public TokenFilter tokenFilter() {
		return new TokenFilter();
	}
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
		return authConfig.getAuthenticationManager();
	}
	/**
	 * 暗码加密办法装备
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new Sm4PasswordEncoder();
	}
+	@Bean
+	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+		// 启用跨域资源共享(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")
public class LoginController {
	private final AuthenticationManager authenticationManager;
	@PostMapping("/login")
	public Result<Map<String, Object>> login(String username, String password) {
		UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
		Authentication authentication = authenticationManager.authenticate(authenticationToken);
		SecurityContextHolder.getContext().setAuthentication(authentication);
		String token = JwtUtil.createToken(username, new HashMap<>());
		AuthUser authUser = (AuthUser) authentication.getPrincipal();
		Map<String, Object> resultMap = new HashMap<>(16);
		resultMap.put("token", token);
		resultMap.put("user", authUser);
		return Result.success(resultMap);
	}
}

login() 办法接纳两个参数:usernamepassword,表示用户输入的用户名和暗码。依据用户名、暗码创立一个UsernamePasswordAuthenticationToken目标,然后调用authenticationManager.authenticate(authenticationToken)办法,运用AuthenticationManager对身份验证令牌进行身份验证,得到一个现已通过身份验证的Authentication目标。然后调用SecurityContextHolder.getContext().setAuthentication(authentication)办法,将验证后的Authentication目标存储到SecurityContextHolder中,以便对用户进行身份认证。调用 JWT 东西类生成 token 。调用authentication.getPrincipal()办法获取经过验证的用户信息,强制类型转换AuthUser类型。统一放在 Map 中回来。

测验

发动项目,运用 ApiPost7 测验登录接口。

认证成功回来成果截图:

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

认证失利回来成果截图:

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