当前位置: 首页 > news >正文

空间购买网站app跟网站的区别

空间购买网站,app跟网站的区别,自学做网站,常州网站建设专业的公司文章目录 说明SpringSecurity认证快速开始设置用户名密码基于application.yml方式基于Java Bean配置方式 设置加密方式自定义用户加载方式自定义登录页面前后端分离认证认证流程 SpringSecurity授权web授权:基于url的访问控制自定义授权失败异常处理方法授权:基于注解的访问控制… 文章目录 说明SpringSecurity认证快速开始设置用户名密码基于application.yml方式基于Java Bean配置方式 设置加密方式自定义用户加载方式自定义登录页面前后端分离认证认证流程 SpringSecurity授权web授权:基于url的访问控制自定义授权失败异常处理方法授权:基于注解的访问控制利用过滤器实现动态权限控制 Spring Security整合JWT自定义登录认证的业务需求JWT详解什么是JWTJWT组成如何应用 自定义登录核心实现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应用 引入依赖 parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.1.4/versionrelativePath/ !-- lookup parent from repository -- /parentdependencies!-- 接入spring security--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.springframework.security/groupIdartifactIdspring-security-test/artifactIdscopetest/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.02308:00 INFO 10172 --- [ main]o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter1f939a0f, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter45658133, org.springframework.security.web.context.SecurityContextHolderFilter1e1eeedd, org.springframework.security.web.header.HeaderWriterFilter1fbf088b, org.springframework.security.web.csrf.CsrfFilter1c3259fa, org.springframework.security.web.authentication.logout.LogoutFilter4c6b4ed7, # 核心Filter校验用户名密码的 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter71ed560f, # 默认登录页面 org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter3aaa3c39, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter430b2699, org.springframework.security.web.authentication.www.BasicAuthenticationFilter40247d48, org.springframework.security.web.savedrequest.RequestCacheAwareFilter7ec95456, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter428bdd72, org.springframework.security.web.authentication.AnonymousAuthenticationFilter67536ae0, # 处理异常的Filter org.springframework.security.web.access.ExceptionTranslationFilter2954f6ab, # Security6把鉴权相关的专门抽出来 成了一个Filter org.springframework.security.web.access.intercept.AuthorizationFilter64c781a9]退出登录 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;MapString, 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(pbkdf2SpringSecurity_v5_8, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put(scrypt, SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());encoders.put(scryptSpringSecurity_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(argon2SpringSecurity_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) {ListGrantedAuthority 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 langen headmeta charsetUTF-8titleTitle/title /head bodyform action/user/login methodpost用户名:input typetext nameusername/br/密码input typepassword namepassword/br/input typesubmit 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;charsetutf-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;charsetutf-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接口 我们先自定义一个类实现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;charsetutf-8);response.getWriter().write(没有访问权限);accessDeniedException.printStackTrace();} }在配置类中指定我们上面创建的类 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;charsetutf-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;charsetutf-8);response.setStatus(HttpStatus.UNAUTHORIZED.value());PrintWriter out response.getWriter();MapString,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 #id10 ) //访问此方法需要具有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 securityJWT 框架实现登录认证授权功能用户登录成功后服务端利用JWT生成token 之后客户端每次访问接口都需要在请求头上添加AuthorizationBearer token 的方式传值到服务器端服务器端再从token中解析和校验token的合法性 如果合法则取出用户数据保存用户信息不需要在校验登录否则就需要重新登录 JWT详解 什么是JWT JSON Web TokenJWT是一个开放的行业标准RFC 7519它定义了一种简介的、自包含的协议格式用于在通信双方传递json对象传递的信息经过数字签名可以被验证和信任。 JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名防止被篡改。 官网 https://jwt.io/ 标准 https://tools.ietf.org/html/rfc7519 JWT令牌的优点 jwt基于json非常方便解析。可以在令牌中自定义丰富的内容易扩展。通过非对称加密算法及数字签名技术JWT防止篡改安全性高。资源服务使用JWT可不依赖授权服务即可完成授权。 缺点 JWT令牌较长占存储空间比较大。 安全性取决于密钥管理 JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理那么 JWT 将会受到攻击。因此在使用 JWT 时一定要注意密钥的管理包括生成、存储、更新、分发等等。 无法撤销 由于 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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.1.4/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.tuling/groupIdartifactIdjwtdemo/artifactIdversion0.0.1-SNAPSHOT/versionnamejwtdemo/namedescriptionjwtdemo/descriptionpropertiesjava.version17/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--JWT依赖--dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactIdversion0.9.1/version/dependencydependencygroupIdjavax.xml.bind/groupIdartifactIdjaxb-api/artifactIdversion2.3.1/version/dependencydependencygroupIdorg.springframework.security/groupIdartifactIdspring-security-jwt/artifactIdversion1.1.1.RELEASE/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion1.2.83/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.springframework.security/groupIdartifactIdspring-security-test/artifactIdscopetest/scope/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-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, MapString, Object claims){// 指定签名的时候使用的签名算法。MacSigner rsaSignernew MacSigner(secret);Jwt jwt JwtHelper.encode(JSON.toJSONString(claims), rsaSigner);return jwt.getEncoded();}public static MapString,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 nowSystem.currentTimeMillis();return nowexpiresIn;} }自定义一个认证成功的处理类当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;charsetutf-8);PrintWriter out response.getWriter();User user (User) principal;//1.从authentication 取出用户信息保存到claims map对象MapString, Object claimsnew 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);MapString,Objectresultnew 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());MapString, 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) 校验当前token是否有效从当前token中取用户信息根据用户信息重新生成一个新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.pierceye.com/news/62563/

相关文章:

  • 军民融合网站建设建设手机网站例
  • 兰州网站建设q479185700強仿虎嗅网 wordpress
  • 做企业云网站的企业织梦网站会员中心模板
  • 手机做网站的网站免费素材图片下载
  • 江西医院网站建设天津城市基础设施建设投资集团有限公司网站
  • 网站前期设计前端开发工程师招聘要求
  • 查看网站主机网站后台任务
  • 做网站公众号要多少钱织梦科技
  • 做一家网站的成本医疗器械行业
  • 服装网站设计策划怎么改wordpress的html5
  • 企业网站规划与设计dw网页制作成品下载
  • 我的世界做封面网站外国人 做的中国字网站
  • 山西网站建设多少钱网站开发 会员模块
  • 网站建设创意文案旅游网站网页设计模板代码
  • 云南做网站报价文章发表有稿费的平台
  • 网站建设公司浩森宇特化德网站建设
  • 做网站的装饰标语中国logo设计公司排名
  • 提供网站建设工具的品牌网站设计会存在什么问题
  • 学网站开发需要多久企业网站设计师
  • 网站建设教程小说简介wordpress手机建站
  • 网站建设基地第三方网络营销平台有哪些
  • 编程网站scratch在线使用湖南网站定制
  • 腾讯网站认证有没有在线做动图的网站
  • 设计招聘专业网站网站ftp需要关闭
  • 天津哪家做企业网站263企业邮箱登录登录入口电脑版
  • 网站建设中 html湛江公司做网站
  • 怎样设网站用nodejs可以做网站么
  • 贸易公司网站设计wordpress 文章附件
  • 农村电子商务网站建设建站技术分享
  • 网站维护中页面设计天津做网站找哪家公司好