基于國密SM4實現(xiàn)用戶認證&授權(quán)
JWT生成和認證的基本流程
JWT(JSON Web Token),是目前比較流行的用戶身份驗證解決方案。 下面是一個簡化的時序圖,用于說明JWT生成和認證的基本流程。
圖片
引入 Spring Security 依賴
Spring Security 是一個功能強大且高度可定制的安全框架。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置說明
在 Spring Security 配置文件中,我們通常需要做如下配置:
- AuthenticationProvider實現(xiàn)類:用于自定義身份驗證邏輯;
- Filter:用于驗證 token 有效性;
- AuthenticationManager:用于接收并處理身份驗證請求;
- PasswordEncoder:用于密碼加密和驗證;
- SecurityFilterChain:過濾器鏈;
圖片
自定義PasswordEncoder
Spring Security的PasswordEncoder是用于進行密碼加密和驗證的接口。它是一個密碼編碼器,用于將用戶的原始密碼轉(zhuǎn)換為安全的加密字符串,并在驗證過程中將加密后的密碼與用戶提供的密碼進行比較。PasswordEncoder接口的主要用于提供安全的密碼存儲和驗證機制,以防止用戶密碼泄露時被惡意使用。它是一種重要的安全性措施,用于保護用戶密碼的安全性。
Spring Security 提供了多種PasswordEncoder接口的實現(xiàn)類,包括:
- BCryptPasswordEncoder:使用BCrypt算法進行密碼哈希和驗證。它是目前廣泛使用的密碼哈希算法之一,具有較高的安全性。
- NoOpPasswordEncoder:不進行任何密碼編碼和哈希操作,即明文存儲密碼。不推薦在生產(chǎn)環(huán)境中使用,僅用于測試目的。
- Pbkdf2PasswordEncoder:使用PBKDF2算法進行密碼哈希和驗證。它通過應(yīng)用哈希函數(shù)多次迭代和鹽值,增加了密碼破解的難度。
- MessageDigestPasswordEncoder:使用指定的消息摘要算法(如MD5、SHA-1、SHA-256等)進行密碼哈希和驗證。
使用國密(SM4)算法
<!-- SM4依賴 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.71</version>
</dependency>
自定義的 PasswordEncoder
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));
}
}
需要實現(xiàn)PasswordEncoder接口的encode()和matches()方法。encode()方法用于對明文密碼進行加密處理,matches()方法用于比較明文密碼與加密后的密碼是否匹配。
配置自定義的 PasswordEncoder
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
// 其它代碼
/**
* 密碼加密方式配置
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new Sm4PasswordEncoder();
}
}
自定義 Filter 驗證 token 有效性
實現(xiàn)UserDetailsService接口,用于獲取用戶詳細信息
import org.yian.springboot.demo.entity.User;
import org.yian.springboot.demo.service.RoleService;
import org.yian.springboot.demo.service.UserService;
import org.yian.springboot.demo.security.model.AuthUser;
import org.yian.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類實現(xiàn)了UserDetailsService接口,重寫了loadUserByUsername方法,用于獲取用戶的詳細信息。
圖片
其中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,添加了一些業(yè)務(wù)屬性。
自定義 Filter 驗證 token 有效性:
import org.yian.springboot.demo.constant.AuthConstant;
import org.yian.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);
// 根據(jù)username獲取用戶信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 創(chuàng)建身份驗證對象
Authentication authentication
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 設(shè)置身份驗證對象
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;
}
}
圖片
配置自定義的自定義Filter:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
// 其它代碼
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
}
配置 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提供的一個身份驗證實現(xiàn)類,它使用數(shù)據(jù)庫中的用戶詳細信息和密碼加密器進行身份驗證。
配置 AuthenticationManager
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
// 其它代碼
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
配置過濾器鏈
自定義類,處理未經(jīng)身份驗證或者身份驗證失敗的用戶訪問受保護資源時的行為。
import org.yian.springboot.demo.api.Result;
import org.yian.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;
/**
* 處理未經(jīng)身份驗證或者身份驗證失敗的用戶訪問受保護資源時的行為
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String msg = StrUtil.format("請求訪問:{},認證失敗,無法訪問系統(tǒng)資源", request.getRequestURI());
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(Result.fail(ResultCode.UNAUTHORIZED, msg)));
}
}
配置AuthenticationManager:
import org.yian.springboot.demo.security.crypto.Sm4PasswordEncoder;
import org.yian.springboot.demo.security.filter.TokenFilter;
import org.yian.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()
// 配置會話管理和會話創(chuàng)建策略:不使用會話
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 配置請求授權(quán)規(guī)則
.authorizeRequests().antMatchers("/api/test/**").permitAll()
.antMatchers("/api/auth/**").permitAll()
// 所有其他請求需要進行身份驗證
.anyRequest().authenticated();
// 配置用戶身份驗證邏輯
http.authenticationProvider(authenticationProvider());
// 在UsernamePasswordAuthenticationFilter過濾器之前添加TokenFilter
http.addFilterBefore(tokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
登錄接口
import org.yian.springboot.demo.api.Result;
import org.yian.springboot.demo.security.model.AuthUser;
import org.yian.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()方法接收兩個參數(shù):username和password,表示用戶輸入的用戶名和密碼。根據(jù)用戶名、密碼創(chuàng)建一個UsernamePasswordAuthenticationToken對象;
- 然后調(diào)用authenticationManager.authenticate(authenticationToken)方法,使用AuthenticationManager對身份驗證令牌進行身份驗證,得到一個已經(jīng)通過身份驗證的Authentication對象;
- 然后調(diào)用SecurityContextHolder.getContext().setAuthentication(authentication)方法,將驗證后的Authentication對象存儲到SecurityContextHolder中,以便對用戶進行身份認證;
- 調(diào)用JWT工具類生成token 。調(diào)用authentication.getPrincipal()方法獲取經(jīng)過驗證的用戶信息,強制類型轉(zhuǎn)換為AuthUser類型。統(tǒng)一放在Map中返回。
測試
認證成功返回結(jié)果截圖:
圖片
認證失敗返回結(jié)果截圖:
圖片