(学习笔记)
SpringSecurity-JWT登录
Json Web Token
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.sec</groupId>
<artifactId>spring-security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-demo</name>
<description>spring-security-demo</description>
<properties>
<java.version>17</java.version>
<hutool.version>5.8.39</hutool.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<druid.version>1.2.25</druid.version>
<mysql.version>9.3.0</mysql.version>
<jwt.version>0.11.5</jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- jdk 11+ 引入可选模块 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-3-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
application:
name: spring-security-demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: root
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: select 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
filters: stat,wall,slf4j
max-pool-prepared-statement-per-connection-size: -1
use-global-data-source-stat: true
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: druid
login-password: druid
allow: 127.0.0.1
#mybatis plus
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sec.demo.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# JWT配置
app:
jwtSecret: jwtSecretKeyjwtSecretKeyjwtSecretKeyjwtSecretKeyjwtSecretKeyjwtSecretKey
jwtExpirationMs: 86400000
1、jwt工具类
@Slf4j
@Getter
@Component
public class JwtUtil {
@Value("${app.jwtSecret}")
private String jwtSecret;
//过期时间,毫秒
@Value("${app.jwtExpirationMs}")
private long jwtExpirationMs;
// 创建签名密钥(使用HS512算法)
private SecretKey getSigningKey() {
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成JWT令牌
public String generateJwtToken(UserDetails userPrincipal) {
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
// 从JWT令牌中获取用户名
public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
/**
* jwt 距离 几分钟之前过期
* @param token
* @param minutes
* @return
*/
public boolean tokenWillExpireInMinutes(String token, long minutes){
minutes = minutes == 0L ? 5 : minutes;
Date expiration = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
long timeToExpiration = expiration.getTime() - new Date().getTime();
// 如果剩余时间少于5分钟,则认为token将在5分钟内过期
return timeToExpiration <= minutes * 60 * 1000;
}
// 验证JWT令牌
public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(authToken);
return true;
} catch (io.jsonwebtoken.security.SignatureException e) { // 使用完全限定名避免冲突
log.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
// 从请求中获取JWT令牌
public String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
2、用户详情实现类
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Map;
public class AuthUser implements UserDetails {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
* 附加信息
*/
private Map<String, Object> extra;
/**
* 权限
*/
private Collection<? extends GrantedAuthority> authorities;
public AuthUser() {
}
public AuthUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
//返回当前账户是否未过期
return true;
}
@Override
public boolean isAccountNonLocked() {
//返回当前用户是否未锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {
//返回当前账户凭证是否未过期
return true;
}
@Override
public boolean isEnabled() {
//返回当前账户是否可用
return true;
}
}
3、实现UserDetailsService
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.sec.demo.entity.User;
import com.sec.demo.mapper.UserMapper;
import jakarta.annotation.Resource;
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 java.util.HashSet;
import java.util.Set;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class).eq(User::getUsername, username));
if (user == null) {
throw new UsernameNotFoundException(String.format("%s.这个用户不存在", username));
}
//权限,todo
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
return new AuthUser(user.getUsername(), user.getPassword(), authorities);
}
}
4、创建 JWT 认证过滤器
import cn.hutool.core.util.StrUtil;
import com.sec.demo.util.JwtUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Calendar;
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtUtil jwtUtil;
@Qualifier("userDetailsServiceImpl")
@Resource
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String jwt = jwtUtil.parseJwt(request);
if (StrUtil.isBlankOrUndefined(jwt)){
log.info("jwt is empty");
filterChain.doFilter(request, response);
return;
}
boolean validateJwtToken = jwtUtil.validateJwtToken(jwt);
if (!validateJwtToken){
log.error("jwt is invalid");
return;
}
String username = jwtUtil.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (null == userDetails){
log.error("userDetails is empty");
return;
}
if (!username.equals(userDetails.getUsername())){
log.error("token和username不匹配");
return;
}
//5分钟之后过期,刷新token
if (jwtUtil.tokenWillExpireInMinutes(jwt, 5)){
handleRefreshToken(userDetails,request, response, filterChain);
return;
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
/**
* 刷新token
* @param request
* @param response
* @param filterChain
*/
private void handleRefreshToken(UserDetails userDetails,HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.generateJwtToken(userDetails);
long expireAt = Calendar.getInstance().getTimeInMillis() + jwtUtil.getJwtExpirationMs();
response.addHeader("Authorization", token);
response.addHeader("expireAt", String.valueOf(expireAt));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
}
}
5、处理未授权访问
import cn.hutool.json.JSONUtil;
import jakarta.servlet.ServletException;
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;
import java.util.HashMap;
import java.util.Map;
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
//获取错误信息: Full authentication is required to access this resource
// String localizedMessage = authException.getLocalizedMessage();
String localizedMessage = "请先登录后再访问";
Map<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("message", localizedMessage);
String json = JSONUtil.toJsonStr(result);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
response.getWriter().flush();
response.getWriter().close();
}
}
6、Spring Security 配置类
import com.sec.demo.security.JwtAuthenticationFilter;
import com.sec.demo.security.MyAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// 配置哪些请求可以被忽略,不被Spring Security保护
return web -> web.ignoring()
.requestMatchers("/login",
"/logout",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/favicon.ico",
"/error");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);
//关闭CSRF保护
http.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
//采用无状态会话管理(SessionCreationPolicy.STATELESS)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
//添加jwt登录授权过滤
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//开启跨域
http.cors(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(){
return new JwtAuthenticationFilter();
}
}
7、创建Controller,处理登录请求
import com.sec.demo.util.JwtUtil;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
@RestController
public class LoginController {
@Resource
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return ResponseEntity
.badRequest()
.body(Map.of("message", "密码错误!"));
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
long expireAt = Calendar.getInstance().getTimeInMillis() + jwtUtil.getJwtExpirationMs();
String token = jwtUtil.generateJwtToken(userDetails);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("expireAt", expireAt);
result.put("username", username);
result.put("message", "登录成功!");
return ResponseEntity.ok(result);
}
}