访问1:localhost:8080/security,返回:需要先认证才能访问(说明没有权限)
访问2:localhost:8080/anonymous,返回:anonymous(说明正常访问)
相关文件如下:
pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
WebSecurityConfiguration:
/**
* Spring Security 配置项
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
public class WebSecurityConfiguration {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
private UserDetailsService userDetailsService;
@Autowired
private ApplicationContext applicationContext;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 搜寻 匿名标记 url: PreAuthorize("hasAnyRole('anonymous')") 和 PreAuthorize("@tsp.check('anonymous')") 和 AnonymousAccess
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
Set<String> anonymousUrls = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class);
PathPatternsRequestCondition pathPatternsCondition = infoEntry.getKey().getPathPatternsCondition();
Set<String> patternList = new HashSet<>();
if (null != pathPatternsCondition){
Set<PathPattern> patterns = pathPatternsCondition.getPatterns();
for (PathPattern pattern : patterns) {
patternList.add(pattern.getPatternString());
}
}
if (null != preAuthorize && preAuthorize.value().toLowerCase().contains("anonymous")) {
anonymousUrls.addAll(patternList);
} else if (null != anonymousAccess && null == preAuthorize) {
anonymousUrls.addAll(patternList);
}
}
httpSecurity
// 禁用basic明文验证
.httpBasic(it -> it.disable())
// 前后端分离架构不需要csrf保护
.csrf(it -> it.disable())
// 禁用默认登录页
.formLogin(it -> it.disable())
// 禁用默认登出页
.logout(it -> it.disable())
// 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
.exceptionHandling(exceptions -> {
// 401
exceptions.authenticationEntryPoint(restAuthenticationEntryPoint);
// 403
exceptions.accessDeniedHandler(restAccessDeniedHandler);
})
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
// 允许匿名访问
.requestMatchers(anonymousUrls.toArray(new String[0])).permitAll()
// 允许所有OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
.requestMatchers("/error").permitAll()
// 允许任意请求被已登录用户访问,不检查Authority
.anyRequest().authenticated())
.authenticationProvider(authenticationProvider())
// 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许所有域名进行跨域调用
config.addAllowedOrigin("*");
// 放行全部原始头信息
config.addAllowedHeader("*");
// 允许所有请求方法跨域调用
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
public UserDetailsService userDetailsService() {
return username -> userDetailsService.loadUserByUsername(username);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
// 设置密码编辑器
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
JwtAuthenticationTokenFilter
/**
* JWT登录授权过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain chain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
response.setCharacterEncoding("utf-8");
if (null == authorization){
// 没有token
chain.doFilter(request, response);
return;
}
try{
if (!authorization.startsWith("Bearer ")){
// token格式不正确
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "token格式不正确");
return;
}
boolean verify = MyJWTUtil.verify(authorization);
if(!verify){
// token格式不正确
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "token验证失败");
return;
}
}catch (Exception e){
// token格式不正确
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "token验证失败");
return;
}
JWT jwt = MyJWTUtil.parseToken(authorization);
Object uid = jwt.getPayload("uid");
// todo 解析JWT获取用户信息
LoginUser loginUser = new LoginUser();
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
}
RestAuthenticationEntryPoint:
/**
* 认证失败处理类
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println(authException == null? "Unauthorized" : "需要先认证才能访问");
response.getWriter().flush();
}
}
RestAccessDeniedHandler:
/**
* 自定义无权访问处理类
*/
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
// response.getWriter()
// .println(accessDeniedException==null?"AccessDenied":accessDeniedException.getMessage());
response.getWriter().println(accessDeniedException == null? "AccessDenied" : "没有访问权限");
response.getWriter().flush();
}
}
AnonymousAccess:
/**
* 用于标记匿名访问方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
MyJWTUtil:
/**
* JWT工具类
*/
public class MyJWTUtil extends JWTUtil {
/**
* 解析JWT Token
*
* @param token token
* @return {@link JWT}
*/
public static boolean verify(String token) {
return verify(token, "LOGIN_TOKEN_KEY_20240410".getBytes());
}
/**
* 解析JWT Token
*
* @param token token
* @return {@link JWT}
*/
public static boolean verify(String token, byte[] key) {
if(StrUtil.isNotEmpty(token)){
if(token.startsWith("Bearer ")){
token = token.split("Bearer ")[1].trim();
}
}
return JWT.of(token).setKey(key).verify();
}
/**
* 解析JWT Token
*
* @param token token
* @return {@link JWT}
*/
public static JWT parseToken(String token) {
if(StrUtil.isNotEmpty(token)){
if(token.startsWith("Bearer ")){
token = token.split("Bearer ")[1].trim();
}
}
return JWT.of(token);
}
public static String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader("Authorization");
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
return requestHeader.substring(7);
}
return null;
}
public static String createToken(String userId) {
Map<String, Object> payload = new HashMap<>(4);
payload.put("uid", userId);
payload.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 8);
return createToken(payload, "LOGIN_TOKEN_KEY_20240410".getBytes());
}
}
DemoController:
@RestController
public class DemoController {
@GetMapping("anonymous")
@AnonymousAccess
public String loadCondition() {
return "anonymous";
}
@GetMapping("security")
public String security() {
return "security";
}
}