一定要熟悉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的。