wordpress网站数据库,wordpress 查询文章,对网站域名销户怎么做,国外商业网站松哥最近辅导了几个小伙伴秋招#xff0c;有小伙伴在面小红书时遇到这个问题#xff0c;这个问题想回答全面还是有些挑战#xff0c;松哥结合之前的一篇旧文和大伙一起来聊聊。
一 无状态登录
1.1 什么是有状态
有状态服务#xff0c;即服务端需要记录每次会话的客户端信…松哥最近辅导了几个小伙伴秋招有小伙伴在面小红书时遇到这个问题这个问题想回答全面还是有些挑战松哥结合之前的一篇旧文和大伙一起来聊聊。
一 无状态登录
1.1 什么是有状态
有状态服务即服务端需要记录每次会话的客户端信息从而识别客户端身份根据用户身份进行请求的处理典型的设计如 Tomcat 中的 Session。例如登录用户登录后我们把用户的信息保存在服务端 session 中并且给用户一个 cookie 值记录对应的 session然后下次请求用户携带 cookie 值来这一步有浏览器自动完成我们就能识别到对应 session从而找到用户的信息。这种方式目前来看最方便但是也有一些缺陷如下
服务端保存大量数据增加服务端压力服务端保存用户状态不支持集群化部署
1.2 什么是无状态
微服务集群中的每个服务对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是服务的无状态性即
服务端不保存任何客户端请求者信息客户端的每次请求必须具备自描述信息通过这些信息识别客户端身份
那么这种无状态性有哪些好处呢
客户端请求不依赖服务端的信息多次请求不需要必须访问到同一台服务器服务端的集群和状态对客户端透明服务端可以任意的迁移和伸缩可以方便的进行集群化部署减小服务端存储压力
1.3 如何实现无状态
无状态登录的流程
首先客户端发送账户名/密码到服务端进行认证认证通过后服务端将用户信息加密并且编码成一个 token一般是 JWT返回给客户端以后客户端每次发送请求都需要携带认证的 token服务端对客户端发送来的 token 进行解密判断是否有效并且获取用户登录信息
二 JWT
2.1 JWT 简介
JWT全称是 Json Web Token 是一种 JSON 风格的轻量级的授权和身份认证规范可实现无状态、分布式的 Web 应用授权 JWT 作为一种规范并没有和某一种语言绑定在一起常用的 Java 实现是 GitHub 上的开源项目 jjwt地址如下https://github.com/jwtk/jjwt
2.2 JWT 数据格式
JWT 包含三部分数据 Header头部通常头部有两部分信息 声明类型这里是JWT加密算法自定义
我们会对头部进行 Base64Url 编码可解码得到第一部分数据。 Payload载荷就是有效数据在官方文档中(RFC7519)这里给了7个示例信息 iss (issuer)表示签发人exp (expiration time)表示token过期时间sub (subject)主题aud (audience)受众nbf (Not Before)生效时间iat (Issued At)签发时间jti (JWT ID)编号
这部分也会采用 Base64Url 编码得到第二部分数据。
Signature签名是整个数据的认证信息。一般根据前两步的数据再加上服务的的密钥secret密钥保存在服务端不能泄露给客户端通过 Header 中配置的加密算法生成。用于验证整个数据完整和可靠性。
生成的数据格式如下图 注意这里的数据通过 . 隔开成了三部分分别对应前面提到的三部分另外这里数据是不换行的图片换行只是为了展示方便而已。
2.3 JWT 交互流程
流程图 步骤翻译
应用程序或客户端向授权服务器请求授权获取到授权后授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源如 API
因为 JWT 签发的 token 中已经包含了用户的身份信息并且每次请求都会携带这样服务的就无需保存用户信息甚至无需去数据库查询这样就完全符合了 RESTful 的无状态规范。
2.4 JWT 存在的问题
说了这么多JWT 也不是天衣无缝由客户端维护登录状态带来的一些问题在这里依然存在举例如下
续签问题这是被很多人诟病的问题之一传统的 cookiesession 的方案天然的支持续签但是 jwt 由于服务端不保存用户状态因此很难完美解决续签问题如果引入 redis虽然可以解决问题但是 jwt 也变得不伦不类了。注销问题由于服务端不再保存用户信息所以一般可以通过修改 secret 来实现注销服务端 secret 修改后已经颁发的未过期的 token 就会认证失败进而实现注销不过毕竟没有传统的注销方便。密码重置密码重置后原本的 token 依然可以访问系统这时候也需要强制修改 secret。基于第 2 点和第 3 点一般建议不同用户取不同 secret。 对于上面提到的这些问题在目前具体的项目实践中更多的是通过引入 Redis 来解决这些问题。 三 实战
说了这么久接下来我们就来看看这个东西到底要怎么用
3.1 环境搭建
首先我们来创建一个 Spring Boot 项目创建时需要添加 Spring Security 依赖创建完成后添加 jjwt 依赖完整的 pom.xml 文件如下
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId
/dependency
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId
/dependency
dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactId
/dependency然后在项目中创建一个简单的 User 对象实现 UserDetails 接口如下
public class User implements UserDetails {private String username;private String password;private ListGrantedAuthority authorities;public String getUsername() {return username;}Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic boolean isEnabled() {return true;}//省略getter/setter
}这个就是我们的用户对象先放着备用再创建一个 HelloController内容如下
RestController
public class HelloController {GetMapping(/hello)public String hello() {return hello jwt !;}GetMapping(/admin)public String admin() {return hello admin !;}
}HelloController 很简单这里有两个接口设计是 /hello 接口可以被具有 user 角色的用户访问而 /admin 接口则可以被具有 admin 角色的用户访问。
3.2 JWT 配置
先准备一个 JWT 操作的工具类
public class JwtUtils {public static final Integer EXPIRE_TIME 7 * 24 * 60 * 60;public static final Integer REDIS_TOKEN_EXPIRE_TIME 30 * 60;public static final String TOKEN_ISSUER javaboy;/*** 生成 JWT 字符串** return*/public static String createJWT() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {Authentication authentication SecurityContextHolder.getContext().getAuthentication();String jws Jwts.builder()//设置头部信息(相当于 JWT 字符串第一部分的内容).header().add(type, JWT).and()//相当于 JWT 字符串第二部分的内容//签名密钥.signWith(getPrivateKey())//设置过期时间.expiration(java.util.Date.from(java.time.Instant.now().plusSeconds(EXPIRE_TIME)))//设置用户名.subject(authentication.getName())//设置用户权限相当于把用户权限转为字符串 admin,user,xxx.claim(authorities, authentication.getAuthorities().stream().map(a - a.getAuthority()).collect(Collectors.joining(,)))//令牌签发时间.issuedAt(new Date()).issuer(TOKEN_ISSUER).compact();return jws;}public static PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {ClassPathResource resource new ClassPathResource(private_key.pem);byte[] privateKeyBytes FileCopyUtils.copyToByteArray(resource.getInputStream());String key new String(privateKeyBytes, StandardCharsets.UTF_8);String privateStr key.replace(-----BEGIN PRIVATE KEY-----, ).replaceAll(System.lineSeparator(), ).replace(-----END PRIVATE KEY-----, );byte[] privateKeyDecodedBytes Base64.getDecoder().decode(privateStr);PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(privateKeyDecodedBytes);KeyFactory keyFactory KeyFactory.getInstance(RSA);PrivateKey privateKey keyFactory.generatePrivate(keySpec);return privateKey;}public static RSAPublicKey getPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {ClassPathResource resource new ClassPathResource(public_key.pem);byte[] publicKeyBytes FileCopyUtils.copyToByteArray(resource.getInputStream());String key new String(publicKeyBytes, StandardCharsets.UTF_8);String publicStr key.replace(-----BEGIN PUBLIC KEY-----, ).replaceAll(System.lineSeparator(), ).replace(-----END PUBLIC KEY-----, );KeyFactory keyFactory KeyFactory.getInstance(RSA);byte[] publicKeyBase64Bytes Base64.getDecoder().decode(publicStr);X509EncodedKeySpec publicKeySpec new X509EncodedKeySpec(publicKeyBase64Bytes);PublicKey rsaPublicKey keyFactory.generatePublic(publicKeySpec);return (RSAPublicKey) rsaPublicKey;}public static Authentication verifyToken(String token) {try {Claims claims Jwts.parser().verifyWith(getPublicKey()).build().parseSignedClaims(token).getPayload();String username claims.getSubject();// user,admin,xxxString authorities claims.get(authorities, String.class);// 解析用户权限ListSimpleGrantedAuthority list Arrays.stream(authorities.split(,)).map(s - new SimpleGrantedAuthority(s)).collect(Collectors.toList());return UsernamePasswordAuthenticationToken.authenticated(username, null, list);} catch (Exception e) {//令牌校验失败return null;}}
}接下来在用户登录成功的时候生成 JWT 字符串并存入到 Redis 中如下
Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(a - a.anyRequest().authenticated()).csrf(c - c.disable()).sessionManagement(s - s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).formLogin(f - f.successHandler((req, resp, auth) - {//登录成功生成 JWT 字符串try {String token JwtUtils.createJWT();//将 JWT 字符串保存到 redis 中redisTemplate.opsForValue().set(auth.getName() : token, auth.getName());// 设置过期时间redisTemplate.expire(auth.getName() : token, JwtUtils.REDIS_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);resp.setContentType(application/json;charsetutf-8);RespBean respBean new RespBean(200, token, null);resp.getWriter().write(new ObjectMapper().writeValueAsString(respBean));} catch (NoSuchAlgorithmException e) {throw new RuntimeException(e);} catch (InvalidKeySpecException e) {throw new RuntimeException(e);}}).permitAll()).logout(l - l.logoutSuccessHandler((req, resp, auth) - {// 删除 redis 中的 tokenString key auth.getName() : req.getHeader(Authorization).replace(Bearer , );System.out.println(key key);redisTemplate.delete(key);resp.getWriter().write(logout success);}).logoutUrl(/logout).permitAll());http.addFilterAfter(new JwtFilter(redisTemplate), SecurityContextHolderFilter.class);return http.build();
}最后再来一个校验的过滤器在用法发送请求的时候校验 JWT 字符串如下
public class JwtFilter extends GenericFilterBean {StringRedisTemplate redisTemplate;public JwtFilter(StringRedisTemplate redisTemplate) {this.redisTemplate redisTemplate;}Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {RespBean result null;HttpServletRequest req (HttpServletRequest) servletRequest;HttpServletResponse resp (HttpServletResponse) servletResponse;resp.setContentType(application/json;charsetutf-8);try {//如果是登录请求则直接放行if (req.getRequestURI().equals(/login)) {filterChain.doFilter(servletRequest, servletResponse);return;}//获取请求头中的tokenString token req.getHeader(Authorization);if (token null) {result new RespBean(500, 令牌为空请求失败, null);PrintWriter out resp.getWriter();out.write(new ObjectMapper().writeValueAsString(result));} else {//验证tokentoken token.replace(Bearer , );//验证token是否有效//校验通过则返回认证后的 Auhtentication 对象否则返回 nullAuthentication authentication JwtUtils.verifyToken(token);String s redisTemplate.opsForValue().get(authentication.getName() : token);if (s null) {// 令牌失效result new RespBean(501, 令牌失效请重新登录, null);resp.getWriter().write(new ObjectMapper().writeValueAsString(result));} else {//令牌有效检查令牌过期时间如果临近过期则为令牌续期Long expire redisTemplate.getExpire(authentication.getName() : token);if (expire ! null expire 10 * 60) {//说明令牌还有 10 分钟过期则重新设置新的过期时间redisTemplate.expire(authentication.getName() : token, Duration.ofSeconds(JwtUtils.REDIS_TOKEN_EXPIRE_TIME));}SecurityContextHolder.getContext().setAuthentication(authentication);filterChain.doFilter(servletRequest, servletResponse);}}} catch (Exception e) {} finally {// 清除上下文SecurityContextHolder.clearContext();}}
}关于这个过滤器我说如下几点
首先从请求头中提取出 Authorization 字段这个字段对应的 value 就是用户的 token。将提取出来的 token 字符串调用 JwtUtils.verifyToken 方法进行验证将验证结果放到当前的 SecurityContext 中然后执行过滤链使请求继续执行下去。
3.3 Spring Security 配置
最后在 Spring Security 中配置这个过滤器。
http.addFilterAfter(new JwtFilter(redisTemplate), SecurityContextHolderFilter.class);搞定
3.4 测试
做完这些之后我们的环境就算完全搭建起来了接下来启动项目然后在 POSTMAN 中进行测试如下 登录成功后返回的字符串就是经过 base64url 转码的 token一共有三部分通过一个 . 隔开我们可以对第一个 . 之前的字符串进行解码即 Header如下 再对两个 . 之间的字符解码即 payload 可以看到我们设置信息由于 base64 并不是加密方案只是一种编码方案因此不建议将敏感的用户信息放到 token 中。
接下来再去访问 /hello 接口注意认证方式选择 Bearer TokenToken 值为刚刚获取到的值如下 可以看到访问成功。
四 总结
这就是 JWT 结合 Spring Security 的一个简单用法。