在基本的通信流程中,我们一般采用Session去存储用户的认证状态。在Spring Security实现用户认证三中讲过,在拿到前端传输过来的用户名和密码之后,会有专门的过滤器UsernamePasswordAuthenticationFilter
处理这部分的需求,并且对认证成功的用户生成Token且存储在Session中。在下次发起请求时,直接从Session中取出同用户名的token进行密码哈希的比较要认证用户。
对于无状态认证,则我们的认证不依赖与服务器端存储的Session的状态。所以无状态认证需要我们每次从前端传输一个包含完整认证信息的Token到服务器端进行自定义的认证过程,这使得服务器无需存储和管理会话数据。常见的无状态认证方法包括 JSON Web Token (JWT)、API Key和 OAuth 2.0。
JWT(JSON Web Token)是一种基于JSON的开放标准(RFC 7519),用于在各方之间传递信息。JWT可以进行数字签名,并且可以选择加密其内容。它定义了一种紧凑和自包含的方式, 可以通过URL、POST参数或HTTP头在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的,但是签名不能保证数据的机密性。JWT 可以使用 HMAC 算法、RSA 或 ECDSA 的公钥/私钥对进行签名。
JWT最常见的用途是用户身份验证。一旦用户登录成功,服务器会生成一个JWT并返回给客户端。客户端将JWT存储在本地(如localStorage或cookie),并在每次请求时将其发送到服务器,服务器通过验证JWT来验证用户身份。
JWT由三个主要部分构成:Header(头部)、Payload(负载)和 Signature(签名)。每个部分都有其特定的作用和结构。
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
.
) 连接起来:base64UrlEncode(header) + "." + base64UrlEncode(payload)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
最终,JWT的格式为:
header.payload.signature
登录认证流程如上。
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.12.3version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.12.3version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.12.3version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
dependency>
<dependency>
<groupId>javax.persistencegroupId>
<artifactId>persistence-apiartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.28version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-3-starterartifactId>
<version>1.2.21version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
package com.song.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.song.cloud.mapper")
public class ServiceSecurityJwt6501 {
public static void main(String[] args) {
SpringApplication.run(ServiceSecurityJwt6501.class, args);
}
}
spring:
application:
name: service-security-jwt
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: root
# 注意修改数据库名字
url: jdbc:mysql://localhost:3306/test? characterEncoding=utf8&useSSL=false&serverTimeZone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
data: # 配置redis
redis:
port: 6379
host: 192.168.62.128
password: 1234
app:
jwt-sign-secret: gOk33w29WESOMEx8vUQLb69AsGhlUb7UmrFwu3g2TOo=
jwt-expiration-milliseconds: 604800000 # 七天过期
server:
port: 6501
logging:
level:
web: debug
org.springframework.security: debug
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.song.cloud.entities # 注意修改成自己的包名
configuration:
map-underscore-to-camel-case: true
用来处理
package com.song.cloud.controller;
import com.song.cloud.entities.User;
import com.song.cloud.service.UserService;
import com.song.cloud.utils.JwtTokenProvider;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Resource
private JwtTokenProvider jwtTokenProvider;
@Resource
private UserService userService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/api/auth")
public String auth(@RequestBody User user){
System.out.println(user);
UserDetails userDetails = userService.loadUserDetail(user);
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
boolean matches = delegatingPasswordEncoder.matches(user.getPasswordHash(), userDetails.getPassword());
if(matches){
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
token.setDetails(userDetails);
System.out.println(token);
//保存token到redis
redisTemplate.opsForValue().set(userDetails.getUsername(), token);
return jwtTokenProvider.generateToken(token);
}
return "fail";
}
}
package com.song.cloud.controller;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@RestController
public class IndexController {
@GetMapping("/")
public Map index() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //脱敏处理
Object credentials = authentication.getCredentials();
HashMap<Object, Object> map = new HashMap<>();
map.put("username", authentication.getName());
map.put("authorities", authorities);
map.put("credentials", credentials);
map.put("details", authentication.getDetails());
map.put("principal", principal);
return map;
}
}
package com.song.cloud.entities;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 表名:t_users_test
*/
@Table(name = "t_users_test")
public class User {
/**
* id
*/
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码hash
*/
@Column(name = "password_hash")
private String passwordHash;
/**
* 是否启用
*/
private Boolean enable;
/**
* 获取id
*
* @return id - id
*/
public Long getId() {
return id;
}
/**
* 设置id
*
* @param id id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 获取用户名
*
* @return username - 用户名
*/
public String getUsername() {
return username;
}
/**
* 设置用户名
*
* @param username 用户名
*/
public void setUsername(String username) {
this.username = username;
}
/**
* 获取密码hash
*
* @return passwordHash - 密码hash
*/
public String getPasswordHash() {
return passwordHash;
}
/**
* 设置密码hash
*
* @param passwordHash 密码hash
*/
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
/**
* 获取是否启用
*
* @return enable - 是否启用
*/
public Boolean getEnable() {
return enable;
}
/**
* 设置是否启用
*
* @param enable 是否启用
*/
public void setEnable(Boolean enable) {
this.enable = enable;
}
@Override
public String toString() {
return "User{" +
"enable=" + enable +
", id=" + id +
", username='" + username + '\'' +
", passwordHash='" + passwordHash + '\'' +
'}';
}
}
package com.song.cloud.service;
import com.song.cloud.entities.User;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
public interface UserService {
UserDetails loadUserDetail(User user);
}
package com.song.cloud.service.impl;
import com.song.cloud.config.DBUserDetailManager;
import com.song.cloud.entities.User;
import com.song.cloud.mapper.UserMapper;
import com.song.cloud.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Resource
private DBUserDetailManager dbUserDetailsManager;
@Resource
private UserMapper userMapper;
@Override
public List<User> list() {
return userMapper.selectAll();
}
@Override
public UserDetails loadUserDetail(User user) {
return dbUserDetailsManager.loadUserByUsername(user.getUsername());
}
}
package com.song.cloud.mapper;
import com.song.cloud.entities.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
注意将相应信息改成自己的。包路径、实体名、mapper类名等。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.song.cloud.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.song.cloud.entities.User">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password_hash" jdbcType="VARCHAR" property="passwordHash" />
<result column="enable" jdbcType="BIT" property="enable" />
</resultMap>
</mapper>
主要用来创建JwtToken的。
package com.song.cloud.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwt-sign-secret}")
private String jwtSignSecret;
@Value("${app.jwt-expiration-milliseconds}")
private long jwtExpirationDate;
// 生成 JWT token
public String generateToken(Authentication authentication) {
// 构建一个JWT,它的注册声明(Subject)设为 username
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);
// 使用适合HMAC-SHA-256算法的密钥对JWT进行签名。
// 将其紧凑压缩为最终的字符串形式。签名后的JWT称为'token'。
String token = Jwts.builder()
.issuer("backend")
.subject(username)
.issuedAt(new Date())
.expiration(expireDate)
.signWith(key())
.compact();
return token;
}
private SecretKey key() {
return Keys.hmacShaKeyFor(
//从base64解码得到byte[]
Decoders.BASE64.decode(jwtSignSecret)
);
}
// 从 Jwt token 获取用户名
public String getUsername(String token) {
Claims claims = Jwts.parser()
.verifyWith(key())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
// 验证 Jwt token
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(key())
.build()
.parse(token);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
} catch (SignatureException e){
logger.error("JWT signature validation fails: {}", e.getMessage());
}
return false;
}
}
package com.song.cloud.config;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import java.util.*;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new JdkSerializationRedisSerializer());
return template;
}
}
实现了SecurityContextRepository
,使用redis存储和管理SecurityContext
。
package com.song.cloud.config;
import com.song.cloud.utils.JwtTokenProvider;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class MyRedisSecurityContextRepository implements SecurityContextRepository {
@Resource
private JwtTokenProvider jwtTokenProvider;
@Resource
private RedisTemplate<String, Object> redisTemplate;
private final SecurityContextHolderStrategy contextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
SecurityContext emptyContext = this.contextHolderStrategy.createEmptyContext();
HttpServletRequest request = requestResponseHolder.getRequest();
String token = request.getHeader("Authorization");
if(!StringUtils.hasText(token)) return emptyContext;
String username = jwtTokenProvider.getUsername(token);
if(username == null) return emptyContext;
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken)redisTemplate.opsForValue().get(username);
emptyContext.setAuthentication(usernamePasswordAuthenticationToken);
return emptyContext;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
System.out.println("saveContext-----------------------------------------");
}
@Override
public boolean containsContext(HttpServletRequest request) {
System.out.println("containsContext------------------------------------");
CharSequence authorization = request.getHeader("Authorization");
return StringUtils.hasText(authorization);
}
}
用于实现数据库认证
package com.song.cloud.config;
import com.song.cloud.entities.User;
import com.song.cloud.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
@Component
public class DBUserDetailManager implements UserDetailsManager, UserDetailsPasswordService, Serializable {
@Resource
private UserMapper userMapper;
private Collection<GrantedAuthority> authorities;
private DBUserDetailManager(ArrayList<GrantedAuthority> authorities){
this.authorities = authorities;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
@Override
public void createUser(UserDetails userDetails) {
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("进入:DBUserDetailManager ");
//查询数据库根据用户名
Example example = new Example(User.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("username", username);
User user = userMapper.selectOneByExample(example);
authorities.add(() -> "rule");
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPasswordHash(),
true,
true,
true,
true,
authorities
);
}
}
配置Spring Security的配置类
package com.song.cloud.config;
import com.song.cloud.handler.JwtAuthenticationEntryPoint;
import com.song.cloud.handler.MyAuthenticationEntryPoint;
import com.song.cloud.handler.MyAuthenticationSuccessHandler;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
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.config.Customizer;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Resource
private MyRedisSecurityContextRepository myRedisSecurityContextRepository;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.anyRequest().authenticated();
}).formLogin(Customizer.withDefaults());
http.sessionManagement(session->{
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
});
http.securityContext(context->{
context.securityContextRepository(myRedisSecurityContextRepository);
});
http.exceptionHandling(exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
});
http.csrf(AbstractHttpConfigurer::disable);
http.cors(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
处理认证失败的请求。
package com.song.cloud.handler;
import cn.hutool.json.JSONUtil;
import com.song.cloud.resp.ResultData;
import com.song.cloud.resp.ReturnCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
System.out.println("进入:MyAuthenticationEntryPoint");
String localizedMessage = authException.getLocalizedMessage();
ResultData<Object> fail = ResultData.fail(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), localizedMessage);
String jsonStr = JSONUtil.toJsonStr(fail);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(jsonStr);
}
}