从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Spring Projects - Java开发流行框架 | Spring Security、Spring Authorization Server(OAuth2.1)、JWT

Spring Projects

综合

  Spring 6.1 新特性 RestClient 似乎取代了RestTemplate的定位。
  @Component注解的类会作为Bean直接注册到Spring容器,而@Bean注解的方法则是通过方法体手动提供Bean实例。
  子线程取认证:SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
  OAuth Vs SSO单点登录 JS无感刷新Token Spring无缝刷新Token
  使用 iframe 时必须写上 Spring Security 的 httpSecurity.headers().frameOptions().disable();
  JavaScript解析JWT时间 - new Date(jwtObj.exp * 1000); // 1731507286 * 1000

Spring控制器默认返回html页面名路径,想返回json数据则需要标注@ResponseBody注解!
Spring的JSON库依赖的是Jackson,故首选该库处理JSON相关功能。@Autowired ObjectMapper om;om.writeValueAsString("test");
若想根据if判断返回页面或数据,可通过request.getRequestDispatcher("页面或数据控制器").forward(request, response)中转下!

    @PostMapping("/main/x")
    public String signupAccount() { // 传值用 request.setAttribute("k", "v");
        if (username.startsWith("testtest")) { return "forward:/main/y.json"; }
        return "main/z.html";
    }
    @ResponseBody @PostMapping(value = "/main/y.json",
            produces = MediaType.APPLICATION_JSON_VALUE)
    public String finishJson() { return "{'status':true}"; }

OAuth2基础

Spring Boot

 

Spring Boot 最佳实践

 

vim Hi.java

package com.example;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication public class Hi { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
@RestController class HelloWorld { @RequestMapping("/") public String helloworld() { return "Hello, World!"; } }




build.gradle

 apply plugin: 'java'
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.3.0.RELEASE'
}

 

Run

  gradle -PrunClassName=com.example.Hi runSingle

  访问网址和默认端口 - http://localhost:8080/


Spring Boot 配置 JSP 视图模板引擎 以及和 Thymeleaf 共存

 


 

Spring Security 最佳实践

 

vim Hi.java

package com.example;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@SpringBootApplication public class Hi { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
@RestController class HelloWorld { @RequestMapping("/") public String helloworld() { return "Hello, World!"; } }

// [可选 - 仅用于Spring Security] @EnableWebSecurity class WSCA extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // http 404 example - http://localhost:8080/new/404 http.authorizeRequests().antMatchers("/new/**").permitAll();

super.configure(http); } }




build.gradle

 apply plugin: 'java'
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.3.0.RELEASE'

// [可选] 启用Form Login表单登录页
// 表单登录默认用户名是user 密码则会输出在控制台 - Using generated security password: 随机生成
implementation 'org.springframework.boot:spring-boot-starter-security:2.3.0.RELEASE'
}




[可选] Spring Security配置文件:
src/main/resources/application.yml

#   更改内置错误页路径 /error 至 /new/error
server:
  error:
    path: /new/error
#   设定默认用户名和密码
spring:
  security:
    user:
      name: user
      password: user



Run
  gradle -PrunClassName=com.example.Hi runSingle

  访问网址和默认端口 - http://localhost:8080/

 


 

Spring CORS跨域:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class SpringCORS {

// 用于Spring Security时应提升该Filter优先级:httpSecurity.cors()... @Bean CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.setAllowCredentials(true); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

 


Spring OAuth2


OAuth 定义了4个角色:
资源拥有者(resource owner) An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user. 资源服务器(resource server) The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens. 客户端(client) An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices). 授权服务器(authorization server) The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.



    OAuth 定义的授权模式:

 

 

 

 

  不支持refresh_token

 

    OAuth 提供了两种Token凭据:

 

      Access Token: 用于访问资源服务器,通常有效期2小时。

 

      Refresh Token:用于刷新Access Token,通常有效期7天,Spring OAuth Server中该值并非JWT格式,不应存储Cookie中,可保存至 localStorage 中 https://q.shanyue.tech/fe/dom/571 。

    刷新Token机制:
      1.定时器刷新 - 会产生无效请求。
      2.请求时检查 - 包装fetch API 或 用Axios拦截器
// fetch("https://example.com") const { fetch: fetchBuiltIn } = window; window.fetch = async (...args) => { let [resource, options] = args; if(resource.startsWith("https://")) { console.log("request interceptor."); } const response = await fetchBuiltIn(resource, options); console.log("response interceptor."); return response; };

Spring Authorization Server

官方实例直接跑通 - https://github.com/spring-projects/spring-authorization-server/tree/main/samples#run-the-sample
OIDC 规范约定对外公开的发现文档 URL - https://accounts.google.com/.well-known/openid-configuration
登出、退出接口 End Session Endpoint - https://blog.csdn.net/weixin_43356507/article/details/144216327
  注意 - 该接口不会使 JWT 类型的Token 失效,故应额外做个黑名单,解析时判断下。

避免127.0.0.1同名cookie覆盖问题 - https://github.com/spring-projects/spring-authorization-server/issues/1321
state参数长度尽量控制在微信接口较低的128字节,即128个英文字母。
Token过期时间设置 - 数据表oauth2_registered_client列token_settings:
  settings.token.access-token-time-to-live  设为2小时即7200秒;
  settings.token.refresh-token-time-to-live 设为1星期即604800秒;
  不会返回refresh_token的过期秒数至客户端,因为即使知道过期也做不了什么,故应根据其HTTP响应值判断是否有效。

注意 - Spring Authorization Server 未实现废弃的 grant_type=password 密码模式!

主要流程由 OAuth2AuthorizationEndpointFilter 处理,通常分为两阶段:
  OAuth2 Authorization Endpoint(取Code) - OAuth2AuthorizationCodeRequestAuthenticationConverter提取Servlet入参为OAuth2AuthorizationCodeRequestAuthenticationToken,返回值若为null,则跳过OAuth2AuthorizationCodeRequestAuthenticationProvider.supports(clazz)的匹配及处理。
  OAuth2 Token Endpoint(用Code换Token) - 同上,但OAuth2AuthorizationCodeAuthenticationConverter及相关类名不含Request字样,且链条间传参extends OAuth2AuthorizationGrantAuthenticationToken,而非extends AbstractAuthenticationToken。
    自定义Token响应体:
      oAuth2AuthorizationServerConfigurer.tokenEndpoint(x -> {
            var ash = new OAuth2AccessTokenResponseAuthenticationSuccessHandler();
            ash.setAccessTokenResponseCustomizer((ac) -> {
                OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = ac.getAuthentication();
                Map additionalParams = new HashMap<>(accessTokenAuthentication.getAdditionalParameters());
                additionalParams.put("our-k", "our-v"); // 自定义额外响应参数
                ac.getAccessTokenResponse().additionalParameters(additionalParams);
            });
            x.accessTokenResponseHandler(ash);
      });


  每条处理链执行完毕后,归 OAuth2AuthorizationEndpointFilter 判定成功与失败,并展现响应。
  AuthenticationConverter 和 AuthenticationProvider 注册链按添加顺序调用。

自定义 OAuth Server 验证方式:
  官方实例 - 可重用授权码阶段,从code换token阶段开始编写注册链。

数据表oauth2_registered_client字段client_authentication_methods支持列表:
  client_secret_basic - 即 grant_type=authorization_code 加验证头:
    "Authorization": "Basic " + window.btoa("client_id value:client_secret value")
    注意 - grant_type=refresh_token等参数只能放application/x-www-form-urlencoded的body中,不支持网址上的QueryString。

  client_secret_post - 即 grant_type=authorization_code 加 post+form-data

  client_secret_jwt - 即 grant_type=authorization_code 加基于 client_secret 生成的 client_assertion=JWTvalue
    client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

  private_key_jwt - 无需 grant_type=authorization_code 阶段,无需 client_secret ,由JWKS校验后直接拿Token。
    密钥对的生成和验证均由OAuth2客户端侧负责。
    client_assertion_type=urn:ietf:params:oauth:grant-type:jwt-bearer
    JWT Key由JWKS签发:
      ClientSettings.builder().tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512).jwkSetUrl("https://client.example.com/jwks").build()

  委托代理用户 - 类似 Google Workspace 成员账户的模拟
    grant_type=urn:ietf:params:oauth:grant-type:token-exchange
    提供 subject_token 令牌换取权限范围较小的令牌,不影响原令牌效用;需传递 client_id 和 client_secret。
    通过 scope、audience、resource 参数来缩减令牌能力;参数 actor_token 为参与者。

  none (public clients) - 需提供username和password; 用于非安全型客户端;不支持刷新Token;即 grant_type=authorization_code 加 response_type=code&code_challenge


防止CSRF攻击:
  防君子用Session(JSESSIONID)的CSRF Token和HTTP Referer来源,防小人用验证码机制。
  解决Invalid CSRF token found:
    authorizationServerSecurityFilterChain(HttpSecurity http)
    defaultSecurityFilterChain(HttpSecurity http)
    以上两处均要设置:
      http.csrf(x -> x.ignoringRequestMatchers(AntPathRequestMatcher.antMatcher("/main/temp/apis/x"),
        AntPathRequestMatcher.antMatcher("/main/temp/apis/y") ));