依赖版本
- JDK 17
- Spring Boot 3.2.0
- Spring Security 6.2.0
工程源码:Gitee
为了能够不需要额外配置就能启动项目,看到配置效果。用例采用模拟数据,可自行修改为对应的ORM操作
编写Spring Security基础配置
导入依赖
<properties>
<java-jwt.version>4.4.0</java-jwt.version>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
测试Spring Security
默认配置下,Spring Security form表单登录的用户名为user,密码启动时在控制台输出。
编写测试Controller
package com.yiyan.study.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口
*/
@RestController
public class SecurityController {
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
}
访问接口测试
编写Spring Security基础文件
创建Spring Security模拟数据
package com.yiyan.study.config;
import lombok.Getter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Spring Security 模拟数据
*/
public class SecurityConstant {
/**
* 模拟用户数据。key:用户名,value:密码
*/
public static final Map<String, String> USER_MAP = new ConcurrentHashMap<>();
/**
* 模拟权限数据。key:接口地址,value:所需权限
*/
public static final Map<String, ConfigAttribute> PERMISSION_MAP = new ConcurrentHashMap<>();
/**
* 用户权限数据。key:用户名,value:权限
*/
public static final Map<String, List<PERMISSION>> USER_PERMISSION_MAP = new ConcurrentHashMap<>();
/**
* 白名单
*/
public static final String[] WHITELIST = {"/login"};
static {
// 填充模拟用户数据
USER_MAP.put("admin", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");
USER_MAP.put("user", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");
// 填充用户权限
USER_PERMISSION_MAP.put("admin", List.of(PERMISSION.ADMIN, PERMISSION.USER));
USER_PERMISSION_MAP.put("user", List.of(PERMISSION.USER));
// 填充接口权限
PERMISSION_MAP.put("/user", new SecurityConfig(PERMISSION.USER.getValue()));
PERMISSION_MAP.put("/admin", new SecurityConfig(PERMISSION.ADMIN.getValue()));
}
/**
* 模拟权限
*/
@Getter
public enum PERMISSION {
ADMIN("admin"), USER("user");
private final String value;
private PERMISSION(String value) {
this.value = value;
}
}
}
实现 UserDetails
package com.yiyan.study.config;
import lombok.Builder;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* Spring Security用户信息
*/
@Data
@Builder
public class SecurityUserDetails implements UserDetails {
private String username;
private String password;
private List<SecurityConstant.PERMISSION> permissions;
public SecurityUserDetails(String username, String password, List<SecurityConstant.PERMISSION> permissions) {
this.username = username;
this.password = password;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}
@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;
}
}
实现UserDetailsService
,重写loadUserByUsername
()方法
package com.yiyan.study.config;
import io.micrometer.common.util.StringUtils;
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.List;
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 获取用户信息
String password = SecurityConstant.USER_MAP.get(username);
if (StringUtils.isBlank(password)) {
throw new UsernameNotFoundException("用户名或密码错误");
}
// 获取用户权限
List<SecurityConstant.PERMISSION> permission = SecurityConstant.USER_PERMISSION_MAP.get(username);
// 返回SecurityUserDetails
return SecurityUserDetails.builder()
.username(username)
.password(password)
.permissions(permission)
.build();
}
}
创建自定义过滤器,用于实现对TOKEN进行鉴权
JWT工具类
package com.yiyan.study.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* JWT工具类
*/
public class JwtUtils {
/**
* 默认JWT标签头
*/
public static final String HEADER = "Authorization";
/**
* JWT配置信息
*/
private static JwtConfig jwtConfig;
private JwtUtils() {
}
/**
* 初始化参数
*
* @param header JWT标签头
* @param tokenHead Token头
* @param issuer 签发者
* @param secretKey 密钥 最小长度:4
* @param expirationTime Token过期时间 单位:秒
* @param issuers 签发者列表 校验签发者时使用
* @param audience 接受者
*/
public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime, List<String> issuers, String audience) {
jwtConfig = new JwtConfig();
jwtConfig.setHeader(StringUtils.isNotBlank(header) ? header : HEADER);
jwtConfig.setTokenHead(tokenHead);
jwtConfig.setIssuer(issuer);
jwtConfig.setSecretKey(secretKey);
jwtConfig.setExpirationTime(expirationTime);
if (CollectionUtils.isEmpty(issuers)) {
issuers = Collections.singletonList(issuer);
}
jwtConfig.setIssuers(issuers);
jwtConfig.setAudience(audience);
jwtConfig.setAlgorithm(Algorithm.HMAC256(jwtConfig.getSecretKey()));
}
/**
* 初始化参数
*/
public static void initialize(String header, String issuer, String secretKey, long expirationTime) {
initialize(header, null, issuer, secretKey, expirationTime, null, null);
}
/**
* 初始化参数
*/
public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime) {
initialize(header, tokenHead, issuer, secretKey, expirationTime, null, null);
}
/**
* 生成 Token
*
* @param subject 主题
* @return Token
*/
public static String generateToken(String subject) {
return generateToken(subject, jwtConfig.getExpirationTime());
}
/**
* 生成 Token
*
* @param subject 主题
* @param expirationTime 过期时间
* @return Token
*/
public static String generateToken(String subject, long expirationTime) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expirationTime * 1000);
return JWT.create()
.withSubject(subject)
.withIssuer(jwtConfig.getIssuer())
.withAudience(jwtConfig.getAudience())
.withIssuedAt(now)
.withExpiresAt(expiration)
.sign(jwtConfig.getAlgorithm());
}
/**
* 获取Token数据体
*/
public static String getTokenContent(String token) {
if (StringUtils.isNotBlank(jwtConfig.getTokenHead())) {
token = token.substring(jwtConfig.getTokenHead().length()).trim();
}
return token;
}
/**
* 验证 Token
*
* @param token token
* @return 验证通过返回true,否则返回false
*/
public static boolean isValidToken(String token) {
try {
token = getTokenContent(token);
Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getSecretKey());
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
// Token验证失败
return false;
}
}
/**
* 判断Token是否过期
*
* @param token token
* @return 过期返回true,否则返回false
*/
public static boolean isTokenExpired(String token) {
try {
token = getTokenContent(token);
Algorithm algorithm = Algorithm.HMAC256(jwtConfig.secretKey);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
Date expirationDate = JWT.decode(token).getExpiresAt();
return expirationDate != null && expirationDate.before(new Date());
} catch (JWTVerificationException exception) {
// Token验证失败
return false;
}
}
/**
* 获取 Token 中的主题
*
* @param token token
* @return 主题
*/
public static String getSubject(String token) {
token = getTokenContent(token);
return JWT.decode(token).getSubject();
}
/**
* 获取当前Jwt配置信息
*/
public static JwtConfig getCurrentConfig() {
return jwtConfig;
}
@Data
public static class JwtConfig {
/**
* JwtToken Header标签
*/
private String header;
/**
* Token头
*/
private String tokenHead;
/**
* 签发者
*/
private String issuer;
/**
* 密钥
*/
private String secretKey;
/**
* Token 过期时间
*/
private long expirationTime;
/**
* 签发者列表
*/
private List<String> issuers;
/**
* 接受者
*/
private String audience;
/**
* 加密算法
*/
private Algorithm algorithm;
}
}
配置JWT
application.yml 添加配置
server:
port: 8080
# ======== JWT配置 ========
jwt:
secret: 1234567890123456
expirationTime: 604800
issuer: springboot3-security
header: Authorization
tokenHead: Bearer
配置JWT启动时加载配置项
package com.yiyan.study.config;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* JWT 配置
*/
@Slf4j
@Component
public class JwtConfig {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.issuer}")
private String issuer;
@Value("${jwt.expirationTime}")
private long expirationTime;
@Value("${jwt.header}")
private String header;
@Value("${jwt.tokenHead}")
private String tokenHead;
@PostConstruct
public void jwtInit() {
JwtUtils.initialize(header, tokenHead, issuer, secretKey, expirationTime);
log.info("JwtUtils初始化完成");
}
}
自定义拦截器
package com.yiyan.study.config;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 自定义过滤器
*/
@Component
public class MyAuthenticationFilter extends OncePerRequestFilter {
@Resource
private SecurityUserDetailsService securityUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader());
// 读取请求头中的token
if (StringUtils.isNotBlank(requestToken)) {
// 判断token是否有效
boolean verifyToken = JwtUtils.isValidToken(requestToken);
if (!verifyToken) {
filterChain.doFilter(request, response);
}
// 解析token中的用户信息
String subject = JwtUtils.getSubject(requestToken);
if (StringUtils.isNotBlank(subject) && SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityUserDetails userDetails = (SecurityUserDetails) securityUserDetailsService.loadUserByUsername(subject);
// 保存用户信息到当前会话
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
// 将authentication填充到安全上下文
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
修改Controller 的登录接口
package com.yiyan.study.controller;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口
*/
@RestController
public class SecurityController {
@Resource
private AuthenticationManager authenticationManager;
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
@GetMapping("/user")
public String helloUser() {
return "Hello User";
}
@GetMapping("/admin")
public String helloAdmin() {
return "Hello Admin";
}
@PostMapping("/login")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 判断是否验证成功
if (null == authentication) {
throw new UsernameNotFoundException("用户名或密码错误");
}
return JwtUtils.generateToken(username);
}
}
编写Spring Security配置文件
Spring Security 升级到6.x后,配置方式与前版本不同,多个旧的配置类被启用。新版本采用lambda表达式的方式进行配置,核心配置项没变化。
package com.yiyan.study.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
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.GrantedAuthority;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Spring Security配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Resource
private UserDetailsService userDetailsService;
@Resource
private MyAuthenticationFilter myAuthenticationFilter;
/**
* 鉴权管理类
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 加密类
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Spring Security 过滤链
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 禁用明文验证
.httpBasic(AbstractHttpConfigurer::disable)
// 关闭csrf
.csrf(AbstractHttpConfigurer::disable)
// 禁用默认登录页
.formLogin(AbstractHttpConfigurer::disable)
// 禁用默认登出页
.logout(AbstractHttpConfigurer::disable)
// 禁用session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置拦截信息
.authorizeHttpRequests(authorization -> authorization
// 允许所有的OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 放行白名单
.requestMatchers(SecurityConstant.WHITELIST).permitAll()
// 根据接口所需权限进行动态鉴权
.anyRequest().access((authentication, object) -> {
// 获取当前的访问路径
String requestURI = object.getRequest().getRequestURI();
PathMatcher pathMatcher = new AntPathMatcher();
// 白名单请求直接放行
for (String url : SecurityConstant.WHITELIST) {
if (pathMatcher.match(url, requestURI)) {
return new AuthorizationDecision(true);
}
}
// 获取访问该路径所需权限
Map<String, ConfigAttribute> permissionMap = SecurityConstant.PERMISSION_MAP;
List<ConfigAttribute> apiNeedPermissions = new ArrayList<>();
for (Map.Entry<String, ConfigAttribute> config : permissionMap.entrySet()) {
if (pathMatcher.match(config.getKey(), requestURI)) {
apiNeedPermissions.add(config.getValue());
}
}
// 如果接口没有配置权限则直接放行
if (apiNeedPermissions.isEmpty()) {
return new AuthorizationDecision(true);
}
// 获取当前登录用户权限信息
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
// 判断当前用户是否有足够的权限访问
for (ConfigAttribute configAttribute : apiNeedPermissions) {
// 将访问所需资源和用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authorities) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
// 权限匹配放行
return new AuthorizationDecision(true);
}
}
}
return new AuthorizationDecision(false);
})
)
// 注册重写后的UserDetailsService实现
.userDetailsService(userDetailsService)
// 注册自定义拦截器
.addFilterBefore(myAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}