微服务安全——SpringSecurity6详解

文章目录

  • 说明
  • SpringSecurity认证
    • 快速开始
    • 设置用户名密码
      • 基于application.yml方式
      • 基于Java Bean配置方式
    • 设置加密方式
    • 自定义用户加载方式
    • 自定义登录页面
    • 前后端分离认证
    • 认证流程
  • SpringSecurity授权
    • web授权:基于url的访问控制
    • 自定义授权失败异常处理
    • 方法授权:基于注解的访问控制
    • 利用过滤器实现动态权限控制
  • Spring Security整合JWT
    • 自定义登录认证的业务需求
    • JWT详解
      • 什么是JWT
      • JWT组成
      • 如何应用
    • 自定义登录核心实现
    • JWT续期问题
      • 刷新令牌(Refresh Token)
      • 自动延长JWT有效期
  • SpringSecurity总结



说明

本文使用的是Security6的版本,先介绍SpringSecurity的使用,然后再去介绍OAuth2。
SpringSecurity也只是入门知识

  • Authentication(认证,解决who are you? )
  • Authorization (访问控制,也就是what are you allowed to do?)

版本:SpringBoot3.1.4、Security6.1.4



SpringSecurity认证

快速开始

创建一个简单的SpringBoot应用

引入依赖

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.4</version><relativePath/> <!-- lookup parent from repository -->
</parent><dependencies><!-- 接入spring security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></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>
</dependencies>



编写一个简单的Controller

@RestController
@RequestMapping("/admin")
public class AdminController {@GetMapping("/demo")public String demo() {return "spring security demo";}
}



启动项目后测试接口调用

引入Spring Security依赖之后 ,访问 API 接口时,需要首先进行登录,才能进行访问。

测试 http://localhost:8080/admin/demo ,会跳转到登录界面

在这里插入图片描述

页面生成源码:DefaultLoginPageGeneratingFilter#generateLoginPageHtml

用户名密码认证Filter: UsernamePasswordAuthenticationFilter



需要登录,默认用户名:user,密码可以查看控制台日志获取

在这里插入图片描述



登录之后跳转回请求接口

在这里插入图片描述



日志中,打印所有要执行的Filter如下

2024-07-22T08:27:34.023+08:00 INFO 10172 --- [           main]o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@1f939a0f, 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@45658133, 
org.springframework.security.web.context.SecurityContextHolderFilter@1e1eeedd, 
org.springframework.security.web.header.HeaderWriterFilter@1fbf088b, 
org.springframework.security.web.csrf.CsrfFilter@1c3259fa, 
org.springframework.security.web.authentication.logout.LogoutFilter@4c6b4ed7, 
# 核心Filter,校验用户名密码的
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@71ed560f,  
# 默认登录页面
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3aaa3c39, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@430b2699, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@40247d48, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7ec95456, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@428bdd72, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@67536ae0, 
# 处理异常的Filter
org.springframework.security.web.access.ExceptionTranslationFilter@2954f6ab, 
# Security6,把鉴权相关的专门抽出来 成了一个Filter
org.springframework.security.web.access.intercept.AuthorizationFilter@64c781a9]



退出登录

Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 http://localhost:8080/logout 退出请求即可。



设置用户名密码

了解即可

基于application.yml方式

可以在application.yml中自定义用户名密码

spring:# Spring Security 配置项,对应 SecurityProperties 配置类security:user:name: user   # 用户名password: 123456  # 密码roles:   # 拥有角色- admin



原理:

默认情况下,UserDetailsServiceAutoConfiguration自动化配置类,会创建一个内存级别的InMemoryUserDetailsManager对象,提供认证的用户信息。

  • 添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会基于配置的信息在内存中创建一个用户User。
  • 未添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会自动在内存中创建一个用户名为 user,密码为 UUID 随机的用户 User



基于Java Bean配置方式

@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {/*** 配置用户信息* 我们正常的用法是自定义一个类,实现UserDetailsService接口,再通过username去查询DB,再封装一个UserDetails对象返回。* 这里就简单实现,直接指定* @return*/@Beanpublic UserDetailsService userDetailsService() {//使用默认加密方式bcrypt对密码进行加密,添加用户信息UserDetails user = User.withDefaultPasswordEncoder().username("hushang").password("123456").roles("user").build();UserDetails admin = User.withUsername("admin").password("{noop}123456") //对密码不加密.roles("admin", "user").build();// 返回一个UserDetailsService的实现类InMemoryUserDetailsManager,从类名可以看出来是基于内存的return new InMemoryUserDetailsManager(user, admin);}
}



另一种方式

@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {/*** 配置用户信息* @return*/@Beanpublic UserDetailsService userDetailsService() {//使用默认加密方式bcrypt对密码进行加密,添加用户信息//加密方式1:{id}encodedPassword ,id为加密算法类型
//        UserDetails user = User.withDefaultPasswordEncoder()
//                .username("hushang")
//                .password("123456")
//                .roles("user")
//                .build();
//
//        UserDetails admin = User.withUsername("admin")
//                .password("{noop}123456") //noop表示对密码不加密
//                .roles("admin", "user")
//                .build();// 加密方式2: passwordEncoder().encode("123456")UserDetails user = User.withUsername("hushang").password(passwordEncoder().encode("123456")).roles("user").build();UserDetails admin = User.withUsername("admin")//指定加密算法对密码加密.password(passwordEncoder().encode("123456")).roles("admin", "user").build();return new InMemoryUserDetailsManager(user, admin);}@Beanpublic PasswordEncoder passwordEncoder(){//return NoOpPasswordEncoder.getInstance();  //不加密return new BCryptPasswordEncoder();  //加密方式bcrypt}
}



设置加密方式

方式1:{id}encodedPassword

Spring Security密码加密格式为:{id}encodedPassword

UserDetails user = User.withUsername("user").password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG").roles("USER").build();
UserDetails admin = User.withUsername("admin").password("{noop}123456") //noop表示对密码不加密.roles("admin", "user").build();



如果密码不指定{id}会抛异常:

在这里插入图片描述



方式2: passwordEncoder().encode(“123456”)

@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withUsername("hushang").password(passwordEncoder().encode("123456")).roles("user").build();return new InMemoryUserDetailsManager(user);}@Beanpublic PasswordEncoder passwordEncoder(){//return NoOpPasswordEncoder.getInstance();  //不加密return new BCryptPasswordEncoder();  //加密方式bcrypt}
}



Spring Security支持的加密方式可以通过PasswordEncoderFactories查看

public final class PasswordEncoderFactories {private PasswordEncoderFactories() {}public static PasswordEncoder createDelegatingPasswordEncoder() {// 默认使用的是BCryptPasswordEncoderString encodingId = "bcrypt";Map<String, PasswordEncoder> encoders = new HashMap();encoders.put(encodingId, new BCryptPasswordEncoder());encoders.put("ldap", new LdapShaPasswordEncoder());encoders.put("MD4", new Md4PasswordEncoder());encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new StandardPasswordEncoder());encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());return new DelegatingPasswordEncoder(encodingId, encoders);}
}



测试类

import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;public static void main(String[] args) {//加密String passwd = BCrypt.hashpw("123456",BCrypt.gensalt());System.out.println(passwd);//校验boolean checkpw = BCrypt.checkpw("123456", "$2a$10$KfdyA40l4iElg7ox9GLR9.4ujIv6q9EfOpcRwrM7zYQrDHZuYoIui");System.out.println(checkpw);BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();// 加密String encode = passwordEncoder.encode("123455");System.out.println(encode);// 校验boolean matches = passwordEncoder.matches("123455", "$2a$10$7ZeilxBWjUfv8XP7tlxZK.GKQPHG4dETOOYPscDZy1lVpK0PLGy96");System.out.println(matches);}



自定义用户加载方式

需要自定义从数据库获取用户信息,可以实现UserDetailsService接口

@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder(){//return NoOpPasswordEncoder.getInstance();  //不加密return new BCryptPasswordEncoder();  //加密方式bcrypt}
}



认证流程中 UsernamePasswordAuthenticationFilter 会找UserDetailsService接口类型,会调用到下面我们重写的方法中,把界面上输入的用户名传递过来

package com.tuling.helloworld.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;@Service
public class TulingUserDetailService implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//TODO  根据用户名可以从数据库获取用户信息,角色以及权限信息// 模拟从数据库获取了用户信息,并封装成UserDetails对象// 这里的API方法和security5没什么改变,UserDetails user = User.withUsername("hushang").password(passwordEncoder.encode("123456")).roles("user").build();return user;}
}/*
直接把密文存入也可以
UserDetails user = User.withUsername("admin").password("$2a$10$KfdyA40l4iElg7ox9GLR9.4ujIv6q9EfOpcRwrM7zYQrDHZuYoIui").roles("user").build();
*/

补充知识点

// 伪代码,roles()指定角色的方法,其实是把我们的角色封装了一下,"ROLE_" + role  封装成了一个Authority权限对象
roles(String... roles) {List<GrantedAuthority> authorities = new ArrayList(roles.length);authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}



复制上一篇笔记中的内容,用户状态的判断

在生产环境下还有可能出现用户被禁用等等这些场景,我们这里也需要考虑进去,就比如某个用户现在数据库中是禁用状态,那么这里就不能让认证通过

org.springframework.security.core.userdetails.User() 对象的构造方法,除了 用户名+密码+一个角色封装之后的权限集合之外

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {this(username, password, true, true, true, true, authorities);}

其实还有一个更为复杂的构造方法

public User(String username,			// 用户名String password,			// 密码boolean enabled,			// 是否可用boolean accountNonExpired,	// 账号过期boolean credentialsNonExpired,	// 凭证过期boolean accountNonLocked,		// 账号锁定Collection<? extends GrantedAuthority> authorities) {......
}

我们可以进行测试,现在将其中一个值该为false,然后测试登录认证

在这里插入图片描述

就会发现即使输入了正确的用户名和密码,还是登录不进去



自定义登录页面

本小节需了解SpringSecurity的过滤器链的配置

Spring Security默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成

编写登录页面

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><form action="/user/login" method="post">用户名:<input type="text" name="username"/><br/>密码:<input type="password" name="password"/><br/><input type="submit" value="提交"/></form>
</body>
</html>



配置Spring Security的过滤器链

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//表单提交// /user/login接口不是我们程序员定义的接口http.formLogin((formLogin) -> formLogin.loginPage("/login.html") //指定自定义登录页面地址.loginProcessingUrl("/user/login")//登录访问路径:前台界面提交表单之后跳转到这个路径进行UserDetailsService的验证,必须和表单提交接口一样.defaultSuccessUrl("/admin/demo")//认证成功之后跳转的路径);//对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests//设置哪些路径可以直接访问,不需要认证.requestMatchers("/login.html","/user/login").permitAll().anyRequest().authenticated() //其他路径的请求都需要认证);//关闭跨站点请求伪造csrf防护http.csrf((csrf) -> csrf.disable());return http.build();
}

测试 http://localhost:8080/admin/demo ,会跳转到自定义登录界面



前后端分离认证

表单登录配置模块提供了successHandler()failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。

其中,successHandler()方法带有一个Authentication参数,携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。

//前后端分离认证逻辑
http.formLogin((formLogin) -> formLogin.loginProcessingUrl("/login") //登录访问接口.successHandler(new LoginSuccessHandler()) //登录成功处理逻辑.failureHandler(new LoginFailureHandler()) //登录失败处理逻辑
);/*** 认证成功处理逻辑,我们可以在这里生成token返回给前端* Authentication参数,携带当前登录用户名及其角色等信息*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("登录成功");}
}//
/*** 认证失败处理逻辑*/
public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// TODOresponse.setContentType("text/html;charset=utf-8");response.getWriter().write("登录失败");exception.printStackTrace();}
}



认证流程

在这里插入图片描述



SpringSecurity授权

授权的方式包括 web授权和方法授权:

  • web授权是通过url拦截进行授权
  • 方法授权是通过方法拦截进行授权



web授权:基于url的访问控制

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护 ,Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。

import com.tuling.helloworld.handler.BussinessAccessDeniedHandler;
import com.tuling.helloworld.handler.LoginFailureHandler;
import com.tuling.helloworld.handler.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//前后端分离认证逻辑http.formLogin((formLogin) -> formLogin.loginProcessingUrl("/login").successHandler(new LoginSuccessHandler()).failureHandler(new LoginFailureHandler()));//对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests//设置哪些路径可以直接访问,不需要认证.requestMatchers("/login").permitAll()  //不需要认证.requestMatchers("/index").hasRole("user")  //需要user角色,底层会判断是否有ROLE_admin权限.requestMatchers("/index2").hasRole("admin").requestMatchers("/user/**").hasAuthority("user:api") //需要user:api权限.requestMatchers("/order/**").hasAuthority("order:api").anyRequest().authenticated()  //其他路径的请求都需要认证,仅仅认证通过后就可以了,不会去进行鉴权);return http.build();}@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withDefaultPasswordEncoder().username("hushang").password("123456").roles("user").build();UserDetails admin = User.withDefaultPasswordEncoder().username("admin").password("123456")// 注意: roles和authorities不能同时配置,同时配置后者会覆盖前者的权限// roles()方法底层对我们角色加一个前缀ROLE_,然后还是调用的authorities()。而authorities()每次都是生成一个新数组赋值.authorities("ROLE_admin","ROLE_user","user:api","order:api").build();return new InMemoryUserDetailsManager(user,admin);}
}



自定义授权失败异常处理

使用 Spring Security 时经常会看见 403(无权限)。Spring Security 支持自定义权限受限处理,需要实现 AccessDeniedHandler接口

  1. 我们先自定义一个类,实现AccessDeniedHandler接口

    public class BussinessAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("没有访问权限");accessDeniedException.printStackTrace();}
    }
    



  1. 在配置类中指定我们上面创建的类

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//前后端分离认证逻辑http.formLogin((formLogin) -> formLogin.loginProcessingUrl("/login") .successHandler(new LoginSuccessHandler()) .failureHandler(new LoginFailureHandler()) );//对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers("/login").permitAll()  .requestMatchers("/index").hasRole("user")  .requestMatchers("/index2").hasRole("admin").requestMatchers("/user/**").hasAuthority("user:api") .requestMatchers("/order/**").hasAuthority("order:api").anyRequest().authenticated() );//关闭跨站点请求伪造csrf防护http.csrf((csrf) -> csrf.disable());//访问受限后的异常处理http.exceptionHandling((exceptionHandling) ->exceptionHandling.accessDeniedHandler(new BussinessAccessDeniedHandler()));return http.build();}
    



更全面一点的写法如下

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;import java.io.IOException;@Component
public class BussinessAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("没有访问权限");accessDeniedException.printStackTrace();}
}



import com.alibaba.fastjson.JSON;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;@Component
public class LoginAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {// 如果验证失败,统一返回JSON串,并将状态码设置为401,表示未授权response.setContentType("application/json;charset=utf-8");response.setStatus(HttpStatus.UNAUTHORIZED.value());PrintWriter out = response.getWriter();Map<String,Object> data = new HashMap<>();data.put("path", request.getRequestURI());data.put("time", LocalDateTime.now().toString());data.put("errCode", HttpStatus.UNAUTHORIZED.value());data.put("errMsg", HttpStatus.UNAUTHORIZED.getReasonPhrase());out.write(JSON.toJSONString(data));out.flush();out.close();}
}



//访问受限后的异常处理http.exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(loginAuthenticationEntryPoint).accessDeniedHandler(bussinessAccessDeniedHandler));



方法授权:基于注解的访问控制

了解即可,注解的方式用的少

Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
}



接下来是Controller层 注解的使用

//Controller
@RolesAllowed({"ROLE_user","ROLE_admin"})  //配置访问此方法时应该具有的角色
@GetMapping("/index5")
public String index5(){return "index5";
}@Secured("ROLE_admin")    //配置访问此方法时应该具有的角色
@GetMapping("/index6")
public String index6(){return "index6";
}



Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。

@PreAuthorize("hasRole('ROLE_admin') and #id<10 ") //访问此方法需要具有admin角色,同时限制只能查询id小于10的用户
@GetMapping("/findUserById")
public String findById(long id) {//TODO 查询数据库获取用户信息return "success";
}



利用过滤器实现动态权限控制

Spring Security从5.5之后动态权限控制方式已经改变。

5.5之前需要实现接口:

  • FilterInvocationSecurityMetadataSource: 获取访问URL所需要的角色信息
  • AccessDecisionManager: 用于权限校验,失败抛出AccessDeniedException 异常



5.5之后,利用过滤器动态控制权限,在AuthorizationFilter中,只需要实现接口AuthorizationManager,如果没有权限,抛出AccessDeniedException异常



权限校验核心逻辑:

org.springframework.security.web.access.intercept.AuthorizationFilter#doFilter

》org.springframework.security.authorization.AuthorityAuthorizationManager#check

》org.springframework.security.authorization.AuthoritiesAuthorizationManager#isAuthorized



Spring Security整合JWT

接下来的案例是Spring Security整合JWT实现自定义登录认证



自定义登录认证的业务需求

用spring boot + spring security+JWT 框架实现登录认证授权功能,用户登录成功后,服务端利用JWT生成token

之后客户端每次访问接口,都需要在请求头上添加Authorization:Bearer token 的方式传值到服务器端,服务器端再从token中解析和校验token的合法性

如果合法,则取出用户数据,保存用户信息,不需要在校验登录;否则就需要重新登录

在这里插入图片描述



JWT详解

什么是JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。

JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

官网: https://jwt.io/

标准: https://tools.ietf.org/html/rfc7519



JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖授权服务即可完成授权。



缺点:

  1. JWT令牌较长,占存储空间比较大。

  2. 安全性取决于密钥管理

    JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。

  3. 无法撤销

    由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户账户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。



使用 JWT 主要用来做下面两点:

  • 认证(Authorization):这是使用 JWT 最常见的一种情况,一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。
  • 信息交换(Information Exchange):JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。



JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。

在这里插入图片描述



头部(header)

头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。这也可以被表示成一个JSON对象:

{"alg": "HS256","typ": "JWT"
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9



载荷(payload)

第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明(建议但不强制使用)

    iss: jwt签发者

    sub: jwt所面向的用户

    aud: 接收jwt的一方

    exp: jwt的过期时间,这个过期时间必须要大于签发时间

    nbf: 定义在什么时间之前,该jwt都是不可用的.

    iat: jwt的签发时间

    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  • 公共的声明

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

  • 私有的声明

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{"sub": "1234567890","name": "John Doe","iat": 1516239022
}

然后将其进行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ



签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

这个部分需要base64加密后的header、base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);var signature = HMACSHA256(encodedString, 'hushang'); // km962Qj9Dvkjovs-ZNoALRsB4WRBKh-LjSuMe4yiIHs



将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.km962Qj9Dvkjovs-ZNoALRsB4WRBKh-LjSuMe4yiIHs

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。



如何应用

一般是在请求头里加入Authorization,并加上Bearer标注:

fetch('api/user/1', {headers: {'Authorization': 'Bearer ' + token}
})

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:

在这里插入图片描述



自定义登录核心实现

maven依赖

<?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.1.4</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.tuling</groupId><artifactId>jwtdemo</artifactId><version>0.0.1-SNAPSHOT</version><name>jwtdemo</name><description>jwtdemo</description><properties><java.version>17</java.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-web</artifactId></dependency><!--JWT依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId><version>1.1.1.RELEASE</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></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></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>



创建一个JWT的工具类

import com.alibaba.fastjson.JSON;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.MacSigner;import java.util.Map;public class JWTUtils {/*** 创建JWT* @param secret* @param claims 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)* @return*/public static String getAccessToken(String secret, Map<String, Object> claims){// 指定签名的时候使用的签名算法。MacSigner rsaSigner=new MacSigner(secret);Jwt jwt = JwtHelper.encode(JSON.toJSONString(claims), rsaSigner);return jwt.getEncoded();}public static Map<String,Object> parseToken(String token){Jwt jwt = JwtHelper.decode(token);return JSON.parseObject(jwt.getClaims());}/*** 根据传入的token过期时间判断token是否已过期* @param expiresIn* @return true-已过期,false-没有过期*/public static boolean isExpiresIn(long expiresIn){long now=System.currentTimeMillis();return now>expiresIn;}
}



自定义一个认证成功的处理类,当SpringSecurity认证通过后调用的方法,需要在配置类中进行配置

import com.alibaba.fastjson.JSON;
import com.tuling.jwtdemo.utils.JWTUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {private String secret = "123456xxxx"; //秘钥private long expMillis = 3600000 ;//30分钟过期,可根据实际情况自行修改;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Object principal = authentication.getPrincipal();response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();User user= (User) principal;//1.从authentication 取出用户信息,保存到claims map对象Map<String, Object> claims=new HashMap<>();claims.put("username",user.getUsername());claims.put("authorities",user.getAuthorities());claims.put("enabled",user.isEnabled());claims.put("expiresIn",(System.currentTimeMillis()+expMillis));//2.生成tokenString token = JWTUtils.getAccessToken(secret, claims);Map<String,Object>result=new HashMap<>();result.put("accessToken",token);//3.将token以JSON串返回前端out.write(JSON.toJSONString(result));out.flush();out.close();}
}



创建一个SpringSecurity的配置类,主要功能是:

  • .successHandler(loginSuccessHandler) 指定认证成功之后的处理handler类
  • http.addFilterBefore(..) 添加JWT登录拦截器,在登录之前获取token并校验
import com.tuling.jwtdemo.filter.JwtAuthenticationTokenFilter;
import com.tuling.jwtdemo.handler.*;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import java.io.IOException;@Configuration
@EnableWebSecurity
public class WebSecurityConfig {// 登录成功之后处理类@Autowiredprivate LoginSuccessHandler loginSuccessHandler;// 登录失败的处理类,与当前业务关系不大 就不贴代码了@Autowiredprivate LoginFailureHandler loginFailureHandler;// 下面两个是认证和授权失败后自定义处理类,与当前业务关系不大 就不贴代码了@Autowiredprivate LoginAuthenticationEntryPoint loginAuthenticationEntryPoint;@Autowiredprivate BussinessAccessDeniedHandler bussinessAccessDeniedHandler;// 校验JWTtoken的Filter,在下面会有具体代码@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;// 登出相关的handler,与当前业务关系不大 就不贴代码了@Autowiredprivate MyLogoutSuccessHandler myLogoutSuccessHandler;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests//设置哪些路径可以直接访问,不需要认证;哪些路径需要什么什么权限.requestMatchers("/login").permitAll().requestMatchers("/user/**").hasRole("admin").anyRequest().authenticated() //其他路径的请求都需要认证);//自定义登录逻辑http.formLogin((formLogin) -> formLogin//登录访问路径:前台界面提交表单之后跳转到这个路径进行UserDetailsService的验证,必须和表单提交接口一样  // 并且提交的用户名和密码请求字段名必须为username、password;在UsernamePasswordAuthenticationFilter类中写死了.loginProcessingUrl("/login")// 认证成功之后的处理handler类,也就是上面我们自己定义的类.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler));//添加JWT登录拦截器,在登录之前获取token并校验http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//访问受限后的异常处理http.exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(loginAuthenticationEntryPoint).accessDeniedHandler(bussinessAccessDeniedHandler));//自定义退出登录逻辑http.logout((logout) -> logout.logoutSuccessHandler(myLogoutSuccessHandler));//关闭跨站点请求伪造csrf防护http.csrf((csrf) -> csrf.disable());return http.build();}
}



定义JWT 校验token的Filter,主要功能为:

  • 验证token是否过期
  • 从token中取用户信息
  • 因为当前是整合了SpringSecurity,之后还有认证的Filter去处理,所以这里就直接setAuthentication(null)置为null就行了。需要注意的是,我们必须在config配置类中指定JWT 验证token的filter 在 认证用户名密码的filter之前。http.addFilterBefore(...)
import com.tuling.jwtdemo.utils.JWTUtils;
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.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.security.core.context.SecurityContextHolder;import java.io.IOException;
import java.util.Map;@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//1.从请求头中取出token,进行判断,如果没有携带token,则继续往下走其他的其他的filter逻辑String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);if (!StringUtils.hasText(tokenValue)) {filterChain.doFilter(request, response);return;}//2. 校验token//2.1 将token切割前缀“bearer ”,然后使用封装的JWT工具解析token,得到一个map对象String token = tokenValue.substring("bearer ".length());Map<String, Object> map = JWTUtils.parseToken(token);//2.2 取出token中的过期时间,调用JWT工具中封装的过期时间校验,如果token已经过期,则删除登录的用户,继续往下走其他filter逻辑if (JWTUtils.isExpiresIn((long) map.get("expiresIn"))) {//token 已经过期// 因为当前是整合了SpringSecurity,之后还有认证的Filter去处理,所以这里就直接置为null就行了// 当然也可以自定义,比如直接抛自定义的业务异常SecurityContextHolder.getContext().setAuthentication(null);filterChain.doFilter(request, response);return;}String username = (String) map.get("username");if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {// 调用实现了UserDetailsService接口的Service方法,获取用户信息// 当然也可以自己改造,自己写service方法,自己查数据库,自己缓存User信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (userDetails != null && userDetails.isEnabled()) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));log.info("authenticated user {}, setting security context", username);// 设置用户登录状态// 添加了下面这行代码,之后的认证Filter就不会再对当前请求进行认证了SecurityContextHolder.getContext().setAuthentication(authentication);// 其实哪怕设置的全都是null,之后的认证Filter也不会对当前请求进行认证//SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(null,null,null));}}filterChain.doFilter(request, response);}
}



创建一个查询User的service,我这里只是随便写一个UserDetails,正常处理是去查询数据库,然后封装为一个UserDetails对象返回

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//TODO 从数据库获取用户信息return new User("hushang","{noop}123456",AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_user"));}
}



测试效果

启动应用后调用登录接口返回token信息,必须是发送post请求

在这里插入图片描述






重启微服务之后,不带token信息访问接口,返回401,没有权限

在这里插入图片描述





带token信息访问接口,返回正常

在这里插入图片描述



我们也可以自定义登录认证接口

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers("/user/**").hasRole("admin").requestMatchers("/loginXXX").permitAll()  // 对我们自定义的登录接口放行.anyRequest().authenticated() );//自定义登录逻辑http.formLogin((formLogin) -> formLogin.loginProcessingUrl("/loginXXX")  // 自定义登录接口.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler));//......
}



测试:

在这里插入图片描述



JWT续期问题

JWT通常是在用户登录后签发的,用于验证用户身份和授权。

JWT 的有效期限(或称“过期时间”)通常是一段时间(例如1小时),过期后用户需要重新登录以获取新的JWT。然而,在某些情况下,用户可能会在JWT到期之前使用应用程序,这可能会导致应用程序不可用或需要用户重新登录。



JWT的续期都需要重新生成token,区别就是每一次请求都生成新token,或者是仅仅对即将过期/已经过期生成新token



刷新令牌(Refresh Token)

  1. 校验当前token是否有效
  2. 从当前token中取用户信息
  3. 根据用户信息重新生成一个新token返回

这种方式仅仅只是不需要用户重新登录,但是每次请求都会生成新token,都需要前端来保存新的token,覆盖老的token

伪代码如下

// 方法接收一个刷新令牌作为参数
public String refreshAccessToken(String refreshToken) {// 验证该令牌是否有效boolean isValid = validateRefreshToken(refreshToken);if (isValid) {// 获取与令牌关联的用户信息String userId = getUserIdFromRefreshToken(refreshToken);// 生成一个新的JWT访问令牌String newAccessToken = generateAccessToken(userId);return newAccessToken;} else {throw new RuntimeException("Invalid refresh token.");}
}



自动延长JWT有效期

在某些情况下,JWT可以自动延长其有效期。例如,当用户在JWT过期前继续使用应用程序时,应用重新设置token过期时间。

要自动延长JWT有效期,您可以在每次请求时检查JWT的过期时间,并在必要时更新JWT的过期时间。



具体的思路就是:每次请求检查token是否过期,如果过期了则获取用户信息重新生成一个token返回,如果没有过期但即将过期,也是生成一个新token返回

public String getAccessToken(HttpServletRequest request) {// 从请求中提取JWT访问令牌String accessToken = extractAccessTokenFromRequest(request);// 检查JWT的过期时间是否已过期if (isAccessTokenExpired(accessToken)) {// 过期// 从token中获取userid,在重新生成一个tokenString userId = extractUserIdFromAccessToken(accessToken);accessToken = generateNewAccessToken(userId);// 没有过期,检查是不是即将过期} else if (shouldRefreshAccessToken(accessToken)) {String userId = extractUserIdFromAccessToken(accessToken);accessToken = generateNewAccessToken(userId);}return accessToken;
}private boolean isAccessTokenExpired(String accessToken) {// 提取过期时间Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);// 过期时间是否在当前时间之前,其实通俗理解就是如果过期了就返回truereturn expirationTime.before(new Date());
}private boolean shouldRefreshAccessToken(String accessToken) {// 提取过期时间Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);Date currentTime = new Date();// 距离过期的剩余时间long remainingTime = expirationTime.getTime() - currentTime.getTime();// 如果令牌在接下来的5分钟内到期,则需要刷新令牌return remainingTime < 5 * 60 * 1000;
}private String generateNewAccessToken(String userId) {// 重新生成JWT tokenDate expirationTime = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME);String accessToken = generateAccessToken(userId, expirationTime);return accessToken;
}



SpringSecurity总结

我们使用SpringSecurity主要就是做认证和授权

认证

  • 我们先自定义一个认证成功与认证失败的处理handler类,我们会在认证成功handler中生成token返回给前端

  • 在config配置类中指定要登录认证的接口路径,并指定认证成功与认证失败的处理handler类

    http.formLogin((formLogin) -> formLogin.loginProcessingUrl("/login").successHandler(loginSuccessHandler).failureHandler(loginFailureHandler));
    
  • 前端调用登录认证接口时,用户名密码必须是username和password,这是在UsernamePasswordAuthenticationFilter默认值,更改更改需要我们做相应的配置

  • 自定义一个Filter,必须要在认证用户名密码filter之前执行;该Filter校验token,验证通过就跳过后续的认证Filter

    //添加JWT登录拦截器,在登录之前获取token并校验
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 在Filter验证token通过之后,需要在最后添加下面这条语句,用以跳过UsernamePasswordAuthenticationFilter的认证流程
    SecurityContextHolder.getContext().setAuthentication(...)
    



鉴权

  • 自定义查询UserService类,实现UserDetailsService接口,在重写的抽象方法中利用username去查询DB,将权限信息一起封装成UserDetails对象返回

    UserDetailsServiceImpl implements UserDetailsService
    
  • 在config配置类中或者使用注解的方式,定义接口需要的权限

    http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests//设置哪些路径可以直接访问,不需要认证.requestMatchers("/login").permitAll().requestMatchers("/user/**").hasRole("admin").anyRequest().authenticated() //其他路径的请求都需要认证);
    
  • 当有请求时,会在AuthorizationFilter中对该请求进行鉴权

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/874909.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

沐风老师3DMAX一键烘焙插件使用方法

3DMAX一键烘焙插件使用教程 3DMAX一键烘焙插件&#xff1a;从3dMax2021增加了一个新功能是全新的BakingToTexture&#xff08;烘焙到纹理&#xff09;工具。s3DMAX一键烘焙插件是新BakingToToTorTexture的简化前端。它允许用户一键烘焙某些实用程序映射&#xff08;贴图&#x…

MySQL 数据表

InnoDB存储引擎文件 InnoDB存储引擎相关的文件包括重做日志文件、表空间文件。 表空间文件 InnoDB存储引擎在设计上模仿了Oracle&#xff0c;将存储的数据按表空间进行存放。默认配置下&#xff0c;会有一个初始化大小为10MB、名为ibdata1的文件&#xff0c;该文件就是默认的…

FlutterFlame游戏实践#16 | 生命游戏 - 编辑与交互

theme: cyanosis 本文为稀土掘金技术社区首发签约文章&#xff0c;30天内禁止转载&#xff0c;30天后未获授权禁止转载&#xff0c;侵权必究&#xff01; Flutter\&Flame 游戏开发系列前言: 该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲…

Jenkins卡在等待界面解决方法

一、问题 部署jenkins服务器出现Please wait while Jenkins is getting ready to work。 二、原因分析 jenkins里面文件指向国外的官网&#xff0c;因为防火墙的原因连不上。 三、解决方法 将配置文件里面的url换成国内镜像&#xff1a; &#xff08;1&#xff09;修改配…

LLM模型与实践之基于 MindSpore 实现 BERT 对话情绪识别

安装环境 # 该案例在 mindnlp 0.3.1 版本完成适配&#xff0c;如果发现案例跑不通&#xff0c;可以指定mindnlp版本&#xff0c;执行!pip install mindnlp0.3.1 !pip install mindnlp 模型简介 BERT是一种由Google于2018年发布的新型语言模型&#xff0c;它是基于Transforme…

css黑色二级下拉导航菜单

黑色二级下拉导航菜单https://www.bootstrapmb.com/item/14816 body { font-family: Arial, sans-serif; margin: 0; padding: 0; }nav { background-color: #000; /* 导航背景色为黑色 */ }.menu { list-style-type: none; margin: 0; padding: 0; overflow: hidden; }.menu l…

JavaScript(12)——内置对象

JavaScript内部提供的对象&#xff0c;包含各种属性和方法给开发者调用。 Math Math对象是JavaScript提供的一个“数学”对象 包含的方法有&#xff1a; random:生成0-1之间的随机数 ceil&#xff1a;向上取整 floor&#xff1a;向下取整 max&#xff1a;找最大数 min&#…

展馆导览系统架构解析,从需求分析到上线运维

在物质生活日益丰富的当下&#xff0c;人们对精神世界的追求愈发强烈&#xff0c;博物馆、展馆、纪念馆等场所成为人们丰富知识、滋养心灵的热门选择。与此同时&#xff0c;人们对展馆的导航体验也提出了更高要求&#xff0c;展馆导览系统作为一种基于室内外地图相结合的位置引…

Unity显示泰语且兼容泰语音标

前言&#xff1a;使用Unity开发的游戏需要支持泰语本地化&#xff0c;以及解决显示泰语时Unity的bug 目录 1、Text组件显示泰语2、TextMeshPro组件显示泰语 现在很多游戏都需要显示泰语&#xff0c;下面将介绍Unity如何显示泰语&#xff0c;&#xff08;仅介绍Unity字体方面的设…

npm 安装报错(已解决)+ 运行 “wue-cli-service”不是内部或外部命令,也不是可运行的程序(已解决)

首先先说一下我这个项目是3年前的一个项目了&#xff0c;中间也是经过了多个人的修改惨咋了布置多少个人的思想&#xff0c;这这道我手里直接npm都安装不上&#xff0c;在网上也查询了多种方法&#xff0c;终于是找到问题所在了 问题1&#xff1a; 先是npm i 报错在下面图片&…

Microsoft 365 Office BusinessPro LTSC 2024 for Mac( 微软Office办公套件)

Microsoft 365 Office BusinessPro LTSC 2024是一款专为商业用户设计的办公软件套件&#xff0c;它集成了Word、Excel、PowerPoint等核心应用&#xff0c;并特别包含了Microsoft Teams这一强大的协作工具。Teams将聊天、会议、文件共享、任务管理等功能整合到一个平台上&#x…

AI+HPC 部署优化面试范围分享

背景 最近几年生成式AI技术和自动驾驶技术发展发展很快&#xff0c;这些行业对于算法的运行效率有很高的要求&#xff0c;尤其一个模型在训练完成后运行到设备上&#xff0c;需要大量的工作&#xff0c;包括模型的剪枝、蒸馏、压缩、量化、算子优化、系统优化等。 对于传统的…

Go基础编程 - 12 -流程控制

流程控制 1. 条件语句1.1. if...else 语句1.2. switch 语句1.3. select 语句1.3.1. select 语句的通信表达式1.3.2. select 的基特性1.3.3. select 的实现原理1.3.4. 经典用法1.3.4.1 超时控制1.3.4.2 多任务并发控制1.3.4.3 监听多通道消息1.3.4.4 default 实现非堵塞读写 2. …

FPGA读写操作SRAM_CY7C1051DV33

手上有一块sram需要验证下功能是否正常&#xff0c;我门通过fpga来进行读写测试。 1.首先看下芯片手册&#xff0c;我们重点关注时序部分 总结下&#xff0c;就是读写时间不能小于10nS,也就是最高频率100M&#xff0c;所以我们程序设计按100M时钟速率进行设计。注意&#x…

构建稳固与安全的网络环境:从微软蓝屏事件看软件更新流程与应急响应

“微软蓝屏”事件暴露了网络安全哪些问题&#xff1f; 近日&#xff0c;由微软视窗系统软件更新引发的全球性“微软蓝屏”事件&#xff0c;不仅让科技领域为之震动&#xff0c;更是一次对全球IT基础设施韧性与安全性的深刻检验。这次事件源于美国电脑安全技术公司“众击”的一…

2024-07-23 Unity插件 Odin Inspector11 —— 使用 Odin 自定义编辑窗口

文章目录 1 OdinEditorWindow1.1 运作方式1.2 使用特性绘制 OdinEditorWindow1.3 在 OdinEditorWindow 中渲染对象 2 OdinMenuEditorWindow2.1 添加菜单导航栏2.2 添加导航栏示例 ​ Odin Window 可以完整地访问 Odin 绘图系统&#xff0c;不再需要操心 Window 的绘制 方式&am…

BGP选路之Local Preference

原理概述 当一台BGP路由器中存在多条去往同一目标网络的BGP路由时&#xff0c;BGP协议会对这些BGP路由的属性进行比较&#xff0c;以确定去往该目标网络的最优BGP路由。BGP首先比较的是路由信息的首选值&#xff08;PrefVal)&#xff0c;如果 PrefVal相同&#xff0c;就会比较本…

全方位了解智慧校园行政办公的新闻管理功能

在智慧校园的日常运营中&#xff0c;行政办公系统中的新闻公告功能犹如一座沟通的桥梁&#xff0c;连接着校园内外的每一个角落&#xff0c;传递着最新的资讯与动态。它不仅是智慧校园信息发布的平台&#xff0c;更是校园文化与精神风貌的展现窗口&#xff0c;对于增强师生的凝…

JavaWeb(4)JavaScript入门2—— JS的对象和JSON

一、JS的对象 1.声明语法1 通过new Object()直接创建对象 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><ti…

C#与C++交互开发系列(三):深入探讨P/Invoke基础知识

欢迎来到C#与C交互开发系列的第三篇。在这篇博客中&#xff0c;我们将深入探讨P/Invoke&#xff08;Platform Invocation Services&#xff09;的基础知识。P/Invoke是C#调用非托管代码的一种机制&#xff0c;能够让C#直接调用C编写的动态链接库&#xff08;DLL&#xff09;中的…