怎样查网站空间地址,企业邮箱收费,网站开发与支付宝端口连接,海澜之家的网站建设目标一定要熟悉spring security原理和jwt无状态原理#xff0c;理解了才知道代码作用。
在 Spring Security JWT 认证流程中#xff0c;通常的做法是#xff1a;
用户提交用户名和密码Spring Security 认证管理器 (AuthenticationManager) 进行认证如果认证成功#xff0c;生…一定要熟悉spring security原理和jwt无状态原理理解了才知道代码作用。
在 Spring Security JWT 认证流程中通常的做法是
用户提交用户名和密码Spring Security 认证管理器 (AuthenticationManager) 进行认证如果认证成功生成 JWT Token 并返回给用户 更详细一点 用户首次登录 发送 POST /login 请求携带 用户名 密码authenticationManager.authenticate() 认证成功后返回 JWT前端存储 JWT通常是 localStorage 或 sessionStorage 用户访问受保护接口 前端在 Authorization 头中附带 Bearer Token过滤器 JWTFilter 解析 JWT从 数据库 加载 UserDetailsSecurityContextHolder.setAuthentication() 认证成功继续访问资源。
参考链接有
spring security 超详细使用教程接入springboot、前后端分离 - 小程xy - 博客园
SpringSecurityjwt实现权限认证功能_spring security jwt-CSDN博客
1.引入相关依赖。我使用的是springboot3.3.5 springsecurity是6.x的 jwt 0.12.6
dependencies!--用于数据加密,默认启用--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.security/groupIdartifactIdspring-security-crypto/artifactId/dependency
/dependencies!--依赖集中管理--dependencyManagementdependencies!-- 使用jwt进行token验证,包括了三个依赖-- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.12.6/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.12.6/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.12.6/version scoperuntime/scope /dependencydependencies/dependencyManagement2.配置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?logouttrue) // 注销成功后的跳转页面.deleteCookies(JSESSIONID) // 删除指定的 Cookie.permitAll() // 允许所有用户注销).sessionManagement(session - session.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::changeSessionId) // 防止会话固定攻击.maximumSessions(1) // 限制每个用户只能有一个活跃会话.maxSessionsPreventsLogin(false)// 如果为 true禁止新登录为 false允许新登录并终止旧会话.expiredUrl(/login?sessionexpired) // 当会话过期时跳转到的页面);// 关闭 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的方法。
1UserDetailsImpl.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;}
}2UserDetailsServiceImpl.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);ListBaseUsers 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;AutowiredQualifier(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.拦截器实现。
1CustomFilter
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);}
}2JwtAuthenticationTokenFilter
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;AutowiredQualifier(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(----》 JwtAuthenticationTokenFiltertoken验证token为空);filterChain.doFilter(request, response);return;}if (token.startsWith(Bearer )) {System.out.println(----》 JwtAuthenticationTokenFiltertoken格式验证中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 SecureDigestAlgorithmSecretKey, SecretKey ALGORITHM Jwts.SIG.HS256;/*** 私钥 / 生成签名的时候使用的秘钥secret一般可以从本地配置文件中读取切记这个秘钥不能外露只在服务端使用在任何场景都不应该流露出去。* 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。* 应该大于等于 256位(长度32及以上的字符串)并且是随机的字符串*/private final static String SECRET BaseInfoEnum.fiedIdOf(secret).getFiedIdInfo();//Cpj2cc09BRTstcISP5HtEAMxwuFEh-nJiL1mppdsz8klzgs;/*** 秘钥实例,相比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 JwsClaims*/public static JwsClaims 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 MapString, Object token2userInfo(String token){MapString, Object tokenMap new HashMapString, 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 MapString, Object login(HttpServletRequest request) throws Exception{MapString, Object modelMap new HashMapString, 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 这一步AuthenticatedfalseUsernamePasswordAuthenticationToken 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的。