(五)Spring Boot学习——spring security +jwt使用(前后端分离模式)
一定要熟悉spring security原理和jwt无状态原理,理解了才知道代码作用。
在 Spring Security + JWT 认证流程中,通常的做法是:
- 用户提交用户名和密码
- Spring Security 认证管理器 (
AuthenticationManager
) 进行认证 - 如果认证成功,生成 JWT Token 并返回给用户
更详细一点
-
用户首次登录
- 发送
POST /login
请求,携带用户名 + 密码
authenticationManager.authenticate()
认证成功后,返回JWT
- 前端存储
JWT
(通常是localStorage
或sessionStorage
)
- 发送
-
用户访问受保护接口
- 前端在
Authorization
头中附带Bearer Token
- 过滤器
JWTFilter
解析JWT
,从数据库
加载UserDetails
SecurityContextHolder.setAuthentication()
认证成功,继续访问资源。
- 前端在
参考链接有:
spring security 超详细使用教程(接入springboot、前后端分离) - 小程xy - 博客园
SpringSecurity+jwt实现权限认证功能_spring security + jwt-CSDN博客
1.引入相关依赖。我使用的是springboot3.3.5 springsecurity是6.x的 jwt 0.12.6
<dependencies><!--用于数据加密,默认启用--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-crypto</artifactId></dependency>
</dependencies><!--依赖集中管理--><dependencyManagement><dependencies><!-- 使用jwt进行token验证,包括了三个依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.12.6</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency><dependencies></dependencyManagement>
2.配置SecurityConfig.java
package com.x.x.x.config;import com.x.x.x.filter.CustomFilter;
import com.x.x.x.filter.JwtAuthenticationTokenFilter;
import com.x.x.x.security.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configurers.SessionManagementConfigurer;
import org.springframework.security.core.userdetails.User;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
public class SecurityConfig {/*** 用户名和密码也可以在application.properties中设置。* @return*/@Beanpublic UserDetailsService userDetailsService() {// 创建基于内存的用户信息管理器InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();// 创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容manager.createUser(User.withUsername("admin").password("yourpassword").roles("ADMIN").build());return manager;}/*** 认证管理。 jwt的用户验证* @param authConfig* @return* @throws Exception*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}/*** 认证的token过滤器* @return*/@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){return new JwtAuthenticationTokenFilter();}/*** 密码加码* @return*/@Beanpublic PasswordEncoder passwordEncoder() {// 也可用有参构造,取值范围是 4 到 31,默认值为 10。数值越大,加密计算越复杂return new BCryptPasswordEncoder();}/*** 配置过滤链* 配置自动注销功能必须在函数里加UserDetailsService userDetailsService,因为重写了使用数据库认证所以用baseuserserviceimpl* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsServiceImpl userDetailsService) throws Exception {http// 开启授权保护,配置请求授权规则.authorizeHttpRequests(authorize -> authorize.requestMatchers("/login","/mylogin","/druid/**").permitAll() // 不需要认证的地址有哪些 ("/blog/**", "/public/**", "/about").anyRequest() // 对所有请求开启授权保护.authenticated() // 已认证的请求会被自动授权)// 配置自定义登录页面// 本处禁用前端页面,使用功能RESTful风格前后端分离,就是不用登录页面.formLogin(form -> form.disable()).httpBasic(Customizer -> Customizer.disable())// 启用记住我功能。允许用户关闭浏览器后仍然保持登录状态,直到主动注销或者查出设定过期时间//.rememberMe(Customizer.withDefaults()).rememberMe(rememberMe -> rememberMe.key("uniqueAndSecret") // 设置一个密钥.tokenValiditySeconds(2 * 24 * 60 * 60) // 设置 RememberMe token 的有效期.userDetailsService(userDetailsService) // 显式设置 UserDetailsService)// 配置注销功能.logout(logout -> logout.logoutUrl("/perform_logout") // 自定义注销请求路径//.logoutSuccessUrl("/login?logout=true") // 注销成功后的跳转页面.deleteCookies("JSESSIONID") // 删除指定的 Cookie.permitAll() // 允许所有用户注销).sessionManagement(session -> session.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::changeSessionId) // 防止会话固定攻击.maximumSessions(1) // 限制每个用户只能有一个活跃会话.maxSessionsPreventsLogin(false)// 如果为 true,禁止新登录;为 false,允许新登录并终止旧会话.expiredUrl("/login?session=expired") // 当会话过期时跳转到的页面);// 关闭 csrf CSRF(跨站请求伪造)是一种网络攻击,攻击者通过欺骗已登录用户,诱使他们在不知情的情况下向受信任的网站发送请求。http.csrf(csrf -> csrf.disable());// 注册自定义的过滤器CustomFilter// 用于jwt 功能确保过滤器的逻辑在每个请求中只执行一次,非常适合需要对每个请求进行处理的场景http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);//已经在customfilter中重写 http.addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);//授权认证,基于角色在 Spring Security 6.x 版本中,antMatchers() 方法已被移除,取而代之的是使用新的基于 请求匹配器 (RequestMatchers) 的方法/*http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/admin/**").hasRole("ADMIN") // 只有 ADMIN 角色可以访问.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER 和 ADMIN 角色可以访问.anyRequest().authenticated()); // 其他请求需要认证//基于权限的授权,编辑权限还是只读等http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/edit/**").hasAuthority("EDIT_PRIVILEGE") // 仅具有 EDIT_PRIVILEGE 权限的用户可以访问.anyRequest().authenticated()); // 其他请求需要认证*/return http.build();}
}
3.重写loadUserByUsername的方法。
(1)UserDetailsImpl.java
package com.x.x.x.security.service.impl;import com.x.x.x.entity.BaseUsers;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;
import java.util.List;@Data
@AllArgsConstructor
@NoArgsConstructor // 这三个注解可以帮我们自动生成 get、set、有参、无参构造函数
public class UserDetailsImpl implements UserDetails {private BaseUsers baseUsers;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return List.of();}@Overridepublic String getPassword() {return baseUsers.getPassword();}@Overridepublic String getUsername() {return baseUsers.getOaId();}@Overridepublic boolean isAccountNonExpired() { // 检查账户是否 没过期。return true;}@Overridepublic boolean isAccountNonLocked() { // 检查账户是否 没有被锁定。return true;}@Overridepublic boolean isCredentialsNonExpired() { //检查凭据(密码)是否 没过期。return true;}@Overridepublic boolean isEnabled() { // 检查账户是否启用。return true;}// 这个方法是 @Data注解 会自动帮我们生成,用来获取 loadUserByUsername 中最后我们返回的创建UserDetailsImpl对象时传入的User。// 如果你的字段包含 username和password 的话可以用强制类型转换, 把 UserDetailsImpl 转换成 User。如果不能强制类型转换的话就需要用到这个方法了public BaseUsers getUser() {return baseUsers;}
}
(2)UserDetailsServiceImpl.java
package com.x.x.x.security.service.impl;import com.x.x.x.entity.BaseUsers;
import com.x.x.x.service.BaseUsersService;
import org.springframework.beans.factory.annotation.Autowired;
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 UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate BaseUsersService baseUsersService;/*** 重写loadUserByUsername方法* @param username the username identifying the user whose data is required.* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {BaseUsers baseUsers = new BaseUsers();baseUsers.setOaId(username);List<BaseUsers> baseUsersList = baseUsersService.queryUsersList(baseUsers);if (baseUsersList == null || baseUsersList.isEmpty()) {System.out.println("-------------> loadUserByUsername验证失败, "+baseUsers.getOaId()+" 不存在!");throw new UsernameNotFoundException(username);}return new UserDetailsImpl(baseUsersList.get(0)); // UserDetailsImpl 是我们实现的类}
}
4.JwtAuthenticationProvider.java继承重新AuthenticationProvider的authenticate方法。这里注意可能未使用我们继承的userDetailsService,所以使用@Qualifier("")指定
package com.x.x.x.security.handler;import io.micrometer.common.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowired@Qualifier("userDetailsServiceImpl")//需要指定注入的是那个类,避免报错。private UserDetailsService userDetailsService;@Overridepublic Authentication authenticate(Authentication authentication) {String username = String.valueOf(authentication.getPrincipal());String password = String.valueOf(authentication.getCredentials());UserDetails userDetails = userDetailsService.loadUserByUsername(username);System.out.println("-------------> JwtAuthenticationProvider:"+userDetails.getUsername()+","+userDetails.getPassword());if(userDetails != null && StringUtils.isNotBlank(userDetails.getPassword())&& userDetails.getPassword().equals(password)){return new UsernamePasswordAuthenticationToken(username,password,authentication.getAuthorities());}try {throw new Exception("RespCodeEnum.NAME_OR_PASSWORD_ERROR");} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.equals(authentication);}
}
5.拦截器实现。
(1)CustomFilter
package com.x.x.x.filter;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;/*** OncePerRequestFilter 是 Spring Security 提供的一个抽象类,确保在每个请求中只执行一次特定的过滤逻辑。* 它是实现自定义过滤器的基础,通常用于对请求进行预处理或后处理。(实现 JWT 会用到这个接口)* 提供了一种机制,以确保过滤器的逻辑在每个请求中只执行一次,非常适合需要对每个请求进行处理的场景。* 通过继承该类,可以轻松实现自定义过滤器适合用于记录日志、身份验证、权限检查等场景。** 本处继承 OncePerRequestFilter 类,并重写 doFilterInternal 方法。* 但是需要再spring security配置类中注册自定义的过滤器*/
public class CustomFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 自定义过滤逻辑,例如记录请求日志System.out.println("Request URI: " + request.getRequestURI());// 继续执行过滤链filterChain.doFilter(request, response);}
}
(2)JwtAuthenticationTokenFilter
package com.x.x.x.filter;import com.x.x.x.dao.BaseUsersDao;
import io.jsonwebtoken.Claims;
import java.io.IOException;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
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.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.x.x.x.until.JwtUtil;@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {/*** 用于验证账号密码,本处于数据库交互*/@Autowiredprivate BaseUsersDao baseUsersDao;@Autowired@Qualifier("userDetailsServiceImpl")//需要指定注入的是那个类,避免报错。private UserDetailsService userDetailsService;/*** 重写了 OncePerRequestFilter 类中的抽象方法 doFilterInternal。* OncePerRequestFilter 是 Spring Security 提供的一个基础类* ,设计用来确保过滤器在同一个请求中只执行一次。* @param request* @param response* @param filterChain* @throws ServletException* @throws IOException*/@Overrideprotected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {// 获取请求头的验证信息,即前端传回的tokenString token = request.getHeader("Authorization");System.out.println("----》 JwtAuthenticationTokenFilter,验证token过滤器,获取到的token值:"+token);//为空时候继续下一步过滤链,即进行登录认证。后续进行格式验证,如果以bearer开始去掉前面的前缀if (!StringUtils.hasText(token) ) {System.out.println("----》 JwtAuthenticationTokenFilter,token验证:"+"token为空!");filterChain.doFilter(request, response);return;}if (token.startsWith("Bearer ")) {System.out.println("----》 JwtAuthenticationTokenFilter,token格式验证中:"+"token格式以Bearer开头,去掉开头!");token = token.substring(7);}//验证token是否过期boolean isValid = JwtUtil.validateJwtToken(token);//只在util中只验证是否过期了。if (!isValid) {System.out.println("----》 token验证失败,token过期。");response(response, "验证失败");return;}//获取token载荷中的用户信息Claims claims = JwtUtil.parseClaim(token).getPayload();String userid = claims.get("username").toString();//查询数据库中用户信息System.out.println("----》 数据库验证用户信息。"+"userid:"+userid);UserDetails userDetails = userDetailsService.loadUserByUsername(userid);System.out.println("----》 数据库中数据:"+userDetails.getUsername()+","+userDetails.getPassword());//设置安全上下文//创建一个自定义的 UserDetailsImpl 对象,将查询到的用户信息封装。//创建一个 UsernamePasswordAuthenticationToken 对象,表示用户的认证信息// ,并将其设置到 Spring Security 的 SecurityContextHolder 中,以便后续请求能够访问到用户的认证信息。UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 如果是有效的jwt,那么设置该用户为认证后的用户SecurityContextHolder.getContext().setAuthentication(authenticationToken);//继续过滤链System.out.println("----》 jwt过滤器执行完毕!"+authenticationToken);filterChain.doFilter(request, response);}private void response(@NotNull HttpServletResponse response,String error) throws IOException {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 或者使用自定义状态码response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding("UTF-8");response.getWriter().write("{\n" +" \"states\": \""+error+"\",\n" +" \"message\": \"无效token!\"\n" +"}");}}
6.jwt实现
package com.x.x.x.until;import com.x.x.x.enums.BaseInfoEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.*;// @Component将这个类标记为 Spring 组件,允许 Spring 管理该类的生命周期,便于依赖注入。
@Component
public class JwtUtil {/*** 过期时间(单位:秒),4小时为14400s*/public static final int ACCESS_EXPIRE = Integer.parseInt(BaseInfoEnum.fiedIdOf("access_expire").getFiedIdInfo());//14400;/*** 加密算法*/private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;/*** 私钥 / 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露,只在服务端使用,在任何场景都不应该流露出去。* 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。* 应该大于等于 256位(长度32及以上的字符串),并且是随机的字符串*/private final static String SECRET = BaseInfoEnum.fiedIdOf("secret").getFiedIdInfo();//"Cpj2cc09BRTstcISP5HtEAMxwuFEh-nJiL1mppdsz8k@lzgs";/*** 秘钥实例,相比secretkeyspec方法base64编码指定验证方式,该种方式更加简便安全。*/public static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes());/*** jwt签发者*/private final static String JWT_ISS = BaseInfoEnum.fiedIdOf("jwt_iss").getFiedIdInfo();/*** jwt主题*/private final static String SUBJECT = "Peripherals";/*** jwt构建器,生成token* 这些是一组预定义的声明,它们 不是强制性的,而是推荐的 ,以 提供一组有用的、可互操作的声明 。* iss: jwt签发者* sub: jwt所面向的用户* aud: 接收jwt的一方* exp: jwt的过期时间,这个过期时间必须要大于签发时间* nbf: 定义在什么时间之前,该jwt都是不可用的.* iat: jwt的签发时间* jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击*/public static String genAccessToken(String username ,String roleId,String company) {// 令牌idString uuid = UUID.randomUUID().toString();Date exprireDate = Date.from(Instant.now().plusSeconds(ACCESS_EXPIRE));//System.out.println("key:"+KEY);return Jwts.builder()// 设置头部信息header.header().add("typ", "JWT").add("alg", "HS256").and()// 设置自定义负载信息payload.claim("username", username )//.claim("roleId",roleId ).claim("company",company )// 令牌ID.id(uuid)// 过期日期.expiration(exprireDate)// 签发时间.issuedAt(new Date())// 主题.subject(SUBJECT)// 签发者.issuer(JWT_ISS)// 签名.signWith(KEY, ALGORITHM).compact();}/*** 解析token* @param token token* @return Jws<Claims>*/public static Jws<Claims> parseClaim(String token) {return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token);}/*** 获取头部信息* @param token* @return*/public static JwsHeader parseHeader(String token) {return parseClaim(token).getHeader();}/*** 获取载荷信息* @param token* @return*/public static Claims parsePayload(String token) {return parseClaim(token).getPayload();}/*** token验证,token是否过期正确* @param token* @return*/public static boolean validateJwtToken(String token) {try {// 解析 Token,验证签名。验证载荷Claims claims = parseClaim(token).getPayload();//System.out.println("content:---"+claims.get("username"));// 验证声明(例如过期时间)if (claims.getExpiration().before(new Date())) {System.out.println("Token has expired.");return false;}// 在这里可以进行其他自定义验证// 例如检查用户角色、权限等// Token 验证通过return true;} catch (Exception e) {// 验证失败System.out.println("Token validation failed: " + e.getMessage());return false;}}/*** 直接获取到载荷的具体内容* @param token* @return*/public static Map<String, Object> token2userInfo(String token){Map<String, Object> tokenMap = new HashMap<String, Object>();Claims claims = parseClaim(token).getPayload();tokenMap.put("company", claims.get("company"));tokenMap.put("loginName", claims.get("username"));tokenMap.put("roleId", claims.get("roleId"));return tokenMap;}//测试public static void main(String[] args){String token = genAccessToken("123","admin","123");System.out.println("token:"+token);boolean isValid = validateJwtToken(token);System.out.println(isValid);System.out.println(parseHeader(token));System.out.println(parsePayload(token));}}
7.接口实现
/*** 用户登录接口。* 本处调用spring security验证功能。(但本项目是前后端分离的,禁用了security登录页功能,* 因为其重定向默认只能用“GET”方式请求)* @param request* @return* @throws Exception*/@PostMapping("/login")public Map<String, Object> login(HttpServletRequest request) throws Exception{Map<String, Object> modelMap = new HashMap<String, Object>();request.setCharacterEncoding("UTF8");//设置request获取数据的编码方式为utf-8String loginName = HttpServletRequestUtil.getString(request, "loginName");String password = HttpServletRequestUtil.getString(request, "password");if (loginName ==null || loginName.isBlank() || password == null || password.isBlank()){modelMap.put("success", false);modelMap.put("msg", "用户名和密码均不能为空");logger.error("----> 登录失败,用户名和密码为空!");return modelMap;}//认证设置,在后续的方法中,已经设置了连接数据库认证loadUserByUsername//先设置认证authentication 这一步Authenticated=falseUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginName, password);//自动调用loadUserByUsername验证用户名和密码,从数据库中对比查找,如果找到了会返回一个带有认证的封装后的用户,否则会报错,自动处理。(这里我们假设我们配置的security是基于数据库查找的)try{Authentication authenticate = authenticationManager.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authenticate);String token = genAccessToken(loginName,"admin","123");modelMap.put("token",token);modelMap.put("success", true);return modelMap;} catch (Exception e) {modelMap.put("success", false);modelMap.put("msg", "用户名或密码错误");logger.error("----> 登录失败,用户名或密码错误!");return modelMap;}}
这里需要注意:
1.一般是url请求带token,直接验证token,通过则授权,在过滤器JwtAuthenticationTokenFilter中UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 验证结果是true的。
2.不带token则在控制器中对用户密码进行验证,因为在loadUserByUsername方法中设置了对用户名密码的验证,所以使用UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginName, password);后,需要手动使用 Authentication authenticate = authenticationManager.authenticate(authenticationToken);进行验证,验证通过则验证结果是true的。
相关文章:

(五)Spring Boot学习——spring security +jwt使用(前后端分离模式)
一定要熟悉spring security原理和jwt无状态原理,理解了才知道代码作用。 在 Spring Security JWT 认证流程中,通常的做法是: 用户提交用户名和密码Spring Security 认证管理器 (AuthenticationManager) 进行认证如果认证成功,生…...

Java中使用EasyExcel
Java中使用EasyExcel 文章目录 Java中使用EasyExcel一:EasyExcel介绍1.1、核心函数导入数据导出数据 1.2、项目实际应用导入数据导出数据 1.3、相关注解ExcelProperty作用示例 二:EasyExcel使用2.1、导入功能2.2、导出功能 三:EasyExcel完整代…...
前沿科技改变生活新趋势
纳米技术在电子设备制造中的应用越来越广泛。这种技术能够帮助制造更小、更快、更耐用的电子产品。 举个例子,手机的处理器是其核心部件。随着纳米技术的进步,现在的处理器比以前小得多,但功能却更强。这样不仅让手机变得更轻薄,…...
不到一个月,SQLite 3.49.0来了
距离 SQLite 3.48.0 发布不到一个月,SQLite 开发团队于 2025 年 2 月 6 日发布了 SQLite 3.49.0 版本。这更新速度的确让人感动,那么这个版本又有哪些更新呢? 查询优化器 新版本改进了自动索引(query-time index)优化…...
Android车机DIY开发之软件篇(十四)编译i.mx8mplus官方kernel
1.下载 下载地址 2.安装依赖 sudo apt-get update sudo apt-get install build-essential git libncurses5-dev libssl-dev bc sudo apt-get install gcc-aarch64-linux-gnu export CROSS_COMPILEaarch64-linux-gnu- 3.配置 make ARCHarm64 defconfig 4.编译 make ARCHa…...

Mac上搭建宝塔环境并部署PHP项目
安装Docker Desktop》搭建Centos版本的宝塔环境》部署PHP项目 1. 下载Docker for mac 软件:https://www.docker.com/ 或使用终端命令:brew install --cask --appdir/Applications docker 2. 使用命令安装宝塔环境的centos7系统: docker pul…...

3.3.3 VO-O语法- 语法算子(二)
循环遍历 由于VO语言是面向数据集的,其所有隐含的语义中都已经带有了遍历并计算的数据逻辑。因此,VO语言只提供了一种支持循环语法的算子--Loop算子。 Loop算子 Loop算子是一个容器算子,其可以实现对其内部子流程的循环迭代运行。但Loop算…...
安装 Ollama 需要哪些步骤?(windows+mac+linux+二进制+Docker)
安装 Ollama 的步骤根据操作系统不同会有所差异,以下是针对不同操作系统的详细安装指南: Windows 系统 下载安装包:访问 Ollama 官方下载页面,下载适用于 Windows 的安装程序 OllamaSetup.exe。运行安装程序:双击下载的安装包,按照提示完成安装。默认安装路径为 C:\User…...

HCIA项目实践--静态路由的综合实验
八 静态路由综合实验 (1)划分网段 # 192.168.1.0 24#分析:每个路由器存在两个环回接口,可以把两个环回接口分配一个环回地址,所以是四个环回,一个骨干,这样分配,不会出现路由黑洞#19…...

Electron视图进程和主进程通讯
快速创建基于vue的electron项目:quick-start/create-electron - npm 视图线程也就index.html是无法直接访问这个api的(如果没有开启视图层访问nodejs的功能,现在几乎没法直接开启,开启了一堆警告提示) 所以需要通过r…...

Vript-Hard——一个基于高分辨率和详细字幕的视频理解算法
一、概述 多模态学习的最新进展促进了对视频理解和生成模型的研究。随之而来的是,对高分辨率视频和详细说明所建立的高质量数据集的需求激增。然而,由于时间因素的影响,视频与文本的配对不像图像那样容易。准备视频和文本配对是一项困难得多…...
react脚手架搭建react项目使用scss
1.create-react-app 创建的项目,webpack配置默认是隐藏的 ,如果要查看 或修改用npm run eject命令,因为create-react-app脚手架默认已经配置了scss、sass所以不用改webpack配置。如果用less 就需要自己添加配置 2.如果直接使用scss的文件会直接报错&…...

Vue.js 状态管理库Pinia
Pinia Pinia :Vue.js 状态管理库Pinia持久化插件-persist Pinia :Vue.js 状态管理库 Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 要使用Pinia ,先要安装npm install pinia在main.js中导入Pinia 并使用 示例…...
【Stable Diffusion部署至GNU/Linux】安装流程
以下是安装Stable Diffusion的步骤,以Ubuntu 22.04 LTS为例子。 显卡与计算架构介绍 CUDA是NVIDIA GPU的专用并行计算架构 技术层级说明CUDA Toolkit提供GPU编译器(nvcc)、数学库(cuBLAS)等开发工具cuDNN深度神经网络加速库(需单独下载)GPU驱动包含CUDA Driver(需与CUDA …...

【C/C++算法】从浅到深学习---滑动窗口(图文兼备 + 源码详解)
绪论:冲击蓝桥杯一起加油!! 每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论: 本章是算法训练的第二章----滑动窗口,它的本质是双指针算法的衍生所以我将…...

计算机毕业设计SpringBoot+Vue.js房源推荐系统 房价预测 房源大数据分析可视化(源码+文档+运行视频+讲解视频)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
开源机器人+具身智能 解决方案+AI
开源机器人、具身智能(Embodied Intelligence)以及AI技术的结合,可以为机器人领域带来全新的解决方案。以下是这一结合的可能方向和具体方案: 1. 开源机器人平台 开源机器人平台为开发者提供了灵活的基础架构,可以在此基础上结合具身智能和AI技术。以下是一些常用的开源机…...

通过 VBA 在 Excel 中自动提取拼音首字母
在excel里面把表格里的中文提取拼音大写缩写怎么弄 在Excel中,如果你想提取表格中的中文字符并转换为拼音大写缩写(即每个汉字的拼音首字母的大写形式),可以通过以下步骤来实现。这项工作可以分为两个主要部分: 提取拼…...

华硕笔记本怎么一键恢复出厂系统_华硕笔记本一键恢复出厂系统教程
华硕笔记本怎么一键恢复出厂系统? 华硕一键恢复出厂系统是一个安全、高效、方便的恢复方式,让您轻松还原出厂设置,以获得更好的系统性能。如果您的华硕电脑遇到问题,可以使用华硕一键恢复出厂系统功能。下面小编就教大家华硕笔记本…...

Ubuntu 如何安装Snipaste截图软件
在Ubuntu上安装Snipaste-2.10.5-x86_64.AppImage的步骤如下: 1. 下载Snipaste AppImage 首先,从Snipaste的官方网站或GitHub Releases页面下载Snipaste-2.10.5-x86_64.AppImage文件。 2. 赋予执行权限 下载完成后,打开终端并导航到文件所在…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)
升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点,但无自动故障转移能力,Master宕机后需人工切换,期间消息可能无法读取。Slave仅存储数据,无法主动升级为Master响应请求ÿ…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...
CppCon 2015 学习:Time Programming Fundamentals
Civil Time 公历时间 特点: 共 6 个字段: Year(年)Month(月)Day(日)Hour(小时)Minute(分钟)Second(秒) 表示…...

UE5 音效系统
一.音效管理 音乐一般都是WAV,创建一个背景音乐类SoudClass,一个音效类SoundClass。所有的音乐都分为这两个类。再创建一个总音乐类,将上述两个作为它的子类。 接着我们创建一个音乐混合类SoundMix,将上述三个类翻入其中,通过它管理每个音乐…...