依赖版本
- 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()));}
/*** 模拟权限*/@Getterpublic 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;}
@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return permissions.stream().map(permission -> new SimpleGrantedAuthority(permission.getValue())).collect(Collectors.toList());}
@Overridepublic String getPassword() {return password;}
@Overridepublic String getUsername() {return username;}
@Overridepublic boolean isAccountNonExpired() {return true;}
@Overridepublic boolean isAccountNonLocked() {return true;}
@Overridepublic boolean isCredentialsNonExpired() {return true;}
@Overridepublic 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 {@Overridepublic 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);// 返回SecurityUserDetailsreturn 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;}
@Datapublic 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: 1234567890123456expirationTime: 604800issuer: springboot3-securityheader: AuthorizationtokenHead: 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;
@PostConstructpublic 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 {
@Resourceprivate SecurityUserDetailsService securityUserDetailsService;
@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader());// 读取请求头中的tokenif (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 {
@Resourceprivate 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 {
@Resourceprivate UserDetailsService userDetailsService;@Resourceprivate MyAuthenticationFilter myAuthenticationFilter;
/*** 鉴权管理类*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}
/*** 加密类*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
/*** Spring Security 过滤链*/@Beanpublic 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();}
}