用starter实现Oauth2中资源服务的统一配置

一、前言

Oauth2中的资源服务Resource需要验证令牌,就要配置令牌的解码器JwtDecoder,认证服务器的公钥等等。如果有多个资源服务Resource,就要重复配置,比较繁锁。把公共的配置信息抽取出来,制成starter,可以极大地简化操作。

  • 未使用starter的原来配置

用starter实现Oauth2中资源服务的统一配置_第1张图片

用starter实现Oauth2中资源服务的统一配置_第2张图片

二、制作starter

详细步骤参考:自定义启动器 Starter【保姆级教程】

1、完整结构图

用starter实现Oauth2中资源服务的统一配置_第3张图片

2、外部引用模块

  • 名称:tuwer-oauth2-config-spring-boot-starter

  • 普通的 maven 项目

  • 资源服务中引入该模块的依赖即可

  • 模块中只有一个pom.xml文件,其余的都可删除

  • pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.tuwergroupId>
    <artifactId>tuwer-oauth2-config-spring-boot-starterartifactId>
    <version>1.0-SNAPSHOTversion>
    <description>oauth2-config启动器description>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>com.tuwergroupId>
            <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigureartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
    dependencies>
project>

3、自动配置模块

  • 核心模块

  • 名称:tuwer-oauth2-config-spring-boot-starter-autoconfigure

  • spring boot 项目

  • pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.6.7version>
    parent>
    <modelVersion>4.0.0modelVersion>

    <groupId>com.tuwergroupId>
    <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigureartifactId>
    <version>1.0-SNAPSHOTversion>
    <description>oauth2-config启动器自动配置模块description>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.22version>
        dependency>
    dependencies>
project>
  • handler(拒绝访问、认证失败)处理类
package com.tuwer.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * 

拒绝访问处理器

* * @author 土味儿 * Date 2022/5/11 * @version 1.0 */
public class SimpleAccessDeniedHandler implements AccessDeniedHandler { @SneakyThrows @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException ) throws IOException, ServletException { //todo your business HashMap<String, String> map = new HashMap<>(2); map.put("uri", request.getRequestURI()); map.put("msg", "拒绝访问"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
package com.tuwer.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * 

认证失败处理器

* * @author 土味儿 * Date 2022/5/11 * @version 1.0 */
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint { @SneakyThrows @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException { HashMap<String, String> map = new HashMap<>(2); if (authException instanceof InvalidBearerTokenException) { // 令牌失效 System.out.println("token失效"); //todo token处理逻辑 } map.put("uri", request.getRequestURI()); map.put("msg", "认证失败"); if (response.isCommitted()) { return; } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_ACCEPTED); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
  • property(权限、令牌)属性类

    权限属性类:AuthProperty,通过application.yml来配置权限,避免在自动配置类中以硬编码的形式写入权限

package com.tuwer.config.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.CollectionUtils;

import java.util.Set;

/**
 * 

权限属性类

* * @author 土味儿 * Date 2022/9/2 * @version 1.0 */
@Data @ConfigurationProperties(prefix = "resource-auth") public final class AuthProperty { private Authority authority; /** * 权限 */ @Data public static class Authority { private Set<String> roles; private Set<String> scopes; private Set<String> auths; } /** * 组装权限字符串 * 目的:给 hasAnyAuthority() 方法生成参数 * @return */ public String getAllAuth() { StringBuilder res = new StringBuilder(); // 角色 Set<String> roles = this.authority.roles; // 角色非空时 if (!CollectionUtils.isEmpty(roles)) { for (String role : roles) { res.append(role).append("','"); } // 循环结果后,生成类似:x ',' y ',' z ',' } // 范围 Set<String> scopes = this.authority.scopes; // 非空时 if (!CollectionUtils.isEmpty(scopes)) { for (String scope : scopes) { res.append("SCOPE_" + scope).append("','"); } // 循环结果后,生成类似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' } // 细粒度权限 Set<String> auths = this.authority.auths; // 非空时 if (!CollectionUtils.isEmpty(auths)) { for (String auth : auths) { res.append(auth).append("','"); } // 循环结果后,生成类似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' l ',' m ',' n ',' } // 如果res不为空,去掉最后多出的三个字符 ',' int len = res.length(); if (len > 3) { res.delete(len - 3, len); } return res.toString(); } }

用starter实现Oauth2中资源服务的统一配置_第4张图片

用starter实现Oauth2中资源服务的统一配置_第5张图片

package com.tuwer.config.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 

属性配置类

* * @author 土味儿 * Date 2022/5/11 * @version 1.0 */
@Data @ConfigurationProperties(prefix = "jwt") public class JwtProperty { /* ======= 配置示例 ====== # 自定义 jwt 配置 jwt: cert-info: # 证书存放位置 public-key-location: myKey.cer claims: # 令牌的鉴发方:即授权服务器的地址 issuer: http://os:9000 */ /** * 证书信息(内部静态类) * 证书存放位置... */ private CertInfo certInfo; /** * 证书声明(内部静态类) * 发证方... */ private Claims claims; @Data public static class Claims { /** * 发证方 */ private String issuer; /** * 有效期 */ //private Integer expiresAt; } @Data public static class CertInfo { /** * 证书存放位置 */ private String publicKeyLocation; } }
  • 解码器自动配置类
package com.tuwer.config;

import com.nimbusds.jose.jwk.RSAKey;
import com.tuwer.config.property.JwtProperty;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import javax.annotation.Resource;
import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;

/**
 * 

自定义jwt解码器

* * @author 土味儿 * Date 2022/5/11 * @version 1.0 */
@EnableConfigurationProperties(JwtProperty.class) @Configuration public class JwtDecoderConfiguration { /** * 注入 JwtProperties 属性配置类 */ @Resource private JwtProperty jwtProperty; /** * 校验jwt发行者 issuer 是否合法 * * @return the jwt issuer validator */ @Bean JwtIssuerValidator jwtIssuerValidator() { return new JwtIssuerValidator(this.jwtProperty.getClaims().getIssuer()); } /* * * 校验jwt是否过期 * * @return the jwt timestamp validator */ /* @Bean JwtTimestampValidator jwtTimestampValidator() { System.out.println("检测令牌是否过期!"+ LocalDateTime.now()); return new JwtTimestampValidator(Duration.ofSeconds((long) this.jwtProperties.getClaims().getExpiresAt())); }*/ /** * jwt token 委托校验器,集中校验的策略{@link OAuth2TokenValidator} * * // @Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常 * @param tokenValidators the token validators * @return the delegating o auth 2 token validator */ @Primary @Bean({"delegatingTokenValidator"}) public DelegatingOAuth2TokenValidator<Jwt> delegatingTokenValidator(Collection<OAuth2TokenValidator<Jwt>> tokenValidators) { return new DelegatingOAuth2TokenValidator<>(tokenValidators); } /** * 基于Nimbus的jwt解码器,并增加了一些自定义校验策略 * * // @Qualifier 当有多个相同类型的bean存在时,指定注入 * @param validator DelegatingOAuth2TokenValidator 委托token校验器 * @return the jwt decoder */ @SneakyThrows @Bean public JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator") DelegatingOAuth2TokenValidator<Jwt> validator) { // 指定 X.509 类型的证书工厂 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); // 读取cer公钥证书来配置解码器 String publicKeyLocation = this.jwtProperty.getCertInfo().getPublicKeyLocation(); // 获取证书文件输入流 ClassPathResource resource = new ClassPathResource(publicKeyLocation); InputStream inputStream = resource.getInputStream(); // 得到证书 X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream); // 解析 RSAKey rsaKey = RSAKey.parse(certificate); // 得到公钥 RSAPublicKey key = rsaKey.toRSAPublicKey(); // 构造解码器 NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(key).build(); // 注入自定义JWT校验逻辑 nimbusJwtDecoder.setJwtValidator(validator); return nimbusJwtDecoder; } }
  • 主自动配置类:安全权限等
package com.tuwer.config;

import com.tuwer.config.handler.SimpleAccessDeniedHandler;
import com.tuwer.config.handler.SimpleAuthenticationEntryPoint;
import com.tuwer.config.property.AuthProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * 

资源服务器配置

* 当解码器JwtDecoder存在时生效 * * @author 土味儿 * Date 2022/5/11 * @version 1.0 */
@ConditionalOnBean(JwtDecoder.class) @EnableConfigurationProperties(AuthProperty.class) @Configuration public class AutoConfiguration { @Resource private AuthProperty authProperty; /** * 资源管理器配置 * * @param http the http * @return the security filter chain * @throws Exception the exception */ @Bean SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { // 拒绝访问处理器 401 SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler(); // 认证失败处理器 403 SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint(); return http // security的session生成策略改为security不主动创建session即STALELESS .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 允许【pc客户端】或【其它微服务】访问 .authorizeRequests() //.antMatchers("/**").hasAnyAuthority("SCOPE_client_pc","SCOPE_micro_service") // 从配置文件中读取权限信息 .antMatchers("/**").hasAnyAuthority(authProperty.getAllAuth()) // 其余请求都需要认证 .anyRequest().authenticated() .and() // 异常处理 .exceptionHandling(exceptionConfigurer -> exceptionConfigurer // 拒绝访问 .accessDeniedHandler(accessDeniedHandler) // 认证失败 .authenticationEntryPoint(authenticationEntryPoint) ) // 资源服务 .oauth2ResourceServer(resourceServer -> resourceServer .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint) .jwt() ) .build(); } /** * JWT个性化解析 * * @return */ @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 如果不按照规范 解析权限集合Authorities 就需要自定义key // jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes"); // OAuth2 默认前缀是 SCOPE_ Spring Security 是 ROLE_ // jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); // 用户名 可以放sub jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB); return jwtAuthenticationConverter; } /** * 开放一些端点的访问控制 * 不需要认证就可以访问的端口 * @return */ @Bean WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().antMatchers( "/actuator/**" ); } }
  • spring.factories

指明自动配置类的地址,在 resources 目录下编写一个自己的 META-INF\spring.factories;有两个自动配置类,中间用逗号分开

注意点

如果同一个组中有多个starter,自动配置类名称不要相同;如果相同,将只有一个配置类生效,其余的将失效。

如:

  • starterA中:com.tuwer.config.AutoConfiguration

  • starterB中:就不要再用 com.tuwer.config.AutoConfiguration 名称,可以改为 com.tuwer.config.AutoConfigurationB

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.tuwer.config.JwtDecoderConfiguration,\
  com.tuwer.config.AutoConfiguration
  • application.yml

令牌、权限的配置可以放在引用starter的资源服务中;如果每个资源服务的配置都一样,可以放在starter中

# 自定义 jwt 配置(校验jwt)
jwt:
  cert-info:
    # 公钥证书存放位置
    public-key-location: myjks.cer
  claims:
    # 令牌的鉴发方:即授权服务器的地址
    issuer: http://os.com:9000

# 自定义权限配置
resource-auth:
  # 权限
  authority:
    # 角色名称;不用加ROlE_,提取用户角色权限时,自动加
    roles:
    # 授权范围;不用加SCOPE_,保持与认证中心中定义的一致即可;
    # 后台自动加 SCOPE_
    scopes:
      - client_pc
      - micro_service
    # 细粒度权限
    auths:
  • 公钥

把认证中心的公钥文件myjks.cer放到resources目录下

4、install

把starter安装install到本地maven仓库中

用starter实现Oauth2中资源服务的统一配置_第6张图片

用starter实现Oauth2中资源服务的统一配置_第7张图片

三、使用starter

1、引入starter依赖

在资源服务中引入 tuwer-oauth2-config-spring-boot-starter

用starter实现Oauth2中资源服务的统一配置_第8张图片

2、application.yml

用starter实现Oauth2中资源服务的统一配置_第9张图片

3、删除资源服务中原文件

用starter实现Oauth2中资源服务的统一配置_第10张图片

你可能感兴趣的:(#,SpringSecurity,OAuth2,#,SpringBoot,java,spring,boot)