avatar

十六小站

欢迎来到我的个人主页! 期待与您分享我的经验与故事,一起探索技术的无穷可能!

  • 首页
  • NAS专题
  • 关于
Home 微服务之Auth篇
文章

微服务之Auth篇

Posted 13 days ago Updated 13 days ago
By 十六 已删除用户
72~92 min read

auth服务重要是认证授权,签发jwt token使用。

生成密钥对

# 在项目 src/main/resources 下生成 jwt.jks(演示用,生产用更严格的密码/keystore)
keytool -genkeypair \
  -alias jwt \
  -keyalg RSA \
  -keysize 2048 \
  -keystore src/main/resources/jwt.jks \
  -storepass 123456 \
  -keypass 123456 \
  -dname "CN=auth.lljing.com, OU=dev, O=lljing, L=City, ST=State, C=CN" \
  -validity 3650

配置解释:

alias: 密钥轮询表示 ,用于生产环境 ,多组密钥对的时候来获取使用的密钥对

keyalg:签名算法,这里固定RSA,非对称加密

keysize:生成的key的长度

keystore:存放密钥对的文件

storepass : 密钥文件的密码

keypass :单个RSA密钥对的密码

dname:

  • CN Common Name 公共名称。通常是域名、服务名或用户名。例如:CN=auth.example.com。在浏览器证书里就是“通用名”。

  • OU Organizational Unit 部门或单位。例如:OU=dev 表示开发部门。

  • O Organization 组织/公司名称。例如:O=Example Inc.。

  • L Locality 城市 / 地理位置。例如:L=Shanghai。

  • ST State or Province 州/省。例如:ST=Shanghai。

  • C Country 国家代码(ISO 3166-1 alpha-2),例如:中国就是 C=CN。

validity :证书的有效期,单位:天

删除已有密钥

keytool -delete -alias jwt -keystore src/main/resources/jwt.jks -storepass [你实际的密码]

生成密钥对的时候alias不同,则会追加形成密钥箱,如果相同,需要先删除原有的密钥。

定义Jwt编解码(RSA)

注册Bean

这里@Bean可以注册为Spring管理的Bean,方便在使用的时候注入。



import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;

import java.io.InputStream;
import java.security.*;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

@Configuration
public class JwkConfig {

    @Value("${spring.auth.keystore.location}")
    private Resource keystore;

    @Value("${spring.auth.keystore.password}")
    private String keystorePassword;

    @Value("${spring.auth.keystore.alias}")
    private String keyAlias;

    @Value("${spring.auth.keystore.keypass}")
    private String keyPassword;

    @Bean
    public RSAKey rsaKey() throws Exception {
        KeyStore ks = KeyStore.getInstance("JKS");
        try (InputStream in = keystore.getInputStream()) {
            ks.load(in, keystorePassword.toCharArray());
        }

        Key key = ks.getKey(keyAlias, keyPassword.toCharArray());
        Certificate cert = ks.getCertificate(keyAlias);
        RSAPublicKey publicKey = (RSAPublicKey) cert.getPublicKey();
        RSAPrivateKey privateKey = (RSAPrivateKey) key;

        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyUse(KeyUse.SIGNATURE)
                .algorithm(JWSAlgorithm.RS256)
                .keyID(keyAlias)
                .build();
    }

    @Bean
    public KeyPair keyPair() throws Exception {
        KeyStore ks = KeyStore.getInstance("JKS");
        try (InputStream in = keystore.getInputStream()) {
            ks.load(in, keystorePassword.toCharArray());
        }

        Key key = ks.getKey(keyAlias, keyPassword.toCharArray());
        if (!(key instanceof PrivateKey)) {
            throw new IllegalStateException("Not a private key");
        }

        PublicKey publicKey = ks.getCertificate(keyAlias).getPublicKey();
        return new KeyPair(publicKey, (PrivateKey) key);
    }

    /***
     * @desc <Jwt签名配置>
     * @param rsaKey
     * @date 2025年9月23日 11:14
     * @return	org.springframework.security.oauth2.jwt.JwtEncoder
     * @exception
    */
    @Bean
    public JwtEncoder jwtEncoder(RSAKey rsaKey) {
        JWKSet jwkSet = new JWKSet(rsaKey);
        JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
        return new NimbusJwtEncoder(jwkSource);
    }

    /**
     * @desc <JWT令牌解码器配置>
     * @param jwkSource 从JwkConfig中注入的RSAKey
     * @date 2025年9月23日
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        // 创建 JWT Processor
        DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        JWSKeySelector<SecurityContext> keySelector =
                new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
        jwtProcessor.setJWSKeySelector(keySelector);

        return new NimbusJwtDecoder(jwtProcessor);
    }

    /***
     * @desc <jwt秘钥箱配置>
     * @param rsaKey
     * @date 2025年9月23日 11:14
     * @return	com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext>
     * @exception
    */
    @Bean
    public JWKSource<SecurityContext> jwkSource(RSAKey rsaKey) {
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (selector, context) -> selector.select(jwkSet);
    }
}

配置

spring:
  auth:
    keystore:
      location: classpath:jwt.jks
      password: 123456
      alias: jwt
      keypass: 123456

编写认证核心实现

认证主要流程就是这个步骤,需要拓展的时候,只需要添加一个AuthenticationProvider的实现类并在他的supports方法中定义好需要解析的实体类型,对应认证即可进入到当前Provider中。

1. 添加认证负载实体(UPAuthenticationToken)

没增加一个认证需要先添加一个认证实体,集成抽象类AbstractAuthenticationToken ,即可

  • principal方法用于获取用户信息负载,“用户名密码”认证的情况下就可以存放用户名,在认证通过后重新构建Principal用于存放用户信息。

  • credentials验证类,用于存放密码或者验证码

  • authorities 用于存放用户角色和权限信息

  • details 其他认证附加信息


import lombok.Setter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * <用户名密码认证实体>
 *
 * @author:JingLonglong
 * @date:2025/9/22
 */
public class UPAuthenticationToken extends AbstractAuthenticationToken {

    @Setter
    private String username;
    
    @Setter
    private UserDetails userDetails;

    @Setter
    private String password;

    public UPAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
    }

    @Override
    public Object getCredentials() {
        return isAuthenticated() ? null : password;
    }

    @Override
    public Object getPrincipal() {
        return isAuthenticated() ? userDetails : username;
    }
}

2. 创建认证处理类(UPAuthenticationProvider)

这里主要是进行用户认证的核心处理类 ,至于数据从哪里来,后面会交代,这里面添加了验证码的相关逻辑,不需要或者自定义实现的可以去除掉。

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

/**
 * <用户名密码认证处理>
 * <功能详细描述>
 *
 * @author:JingLonglong
 * @date:2025/9/22
 */
@Component
@RequiredArgsConstructor
public class UPAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;

    private final RedisService redisService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       //获取登录信息
        String username = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();
        //这里进行用户名密码判断
        if (username == null || password == null) {
            throw new AuthenticationCredentialsNotFoundException("用户名或密码不能为空");
        }

        if ("admin".equalsIgnoreCase(username) || "admin".equalsIgnoreCase(password)) {
            UsernamePasswordAuthentication successAuthentication = new UsernamePasswordAuthentication(new ArrayList<>());
            successAuthentication.setUsername(username);

            AuthUserInfo authUserInfo = new AuthUserInfo(username, password);
            successAuthentication.setDetails(authUserInfo);
            return successAuthentication;
        }
        throw new BadCredentialsException("用户认证失败");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //这里会根据不同的实体类,分配不同的处理。UPAuthenticationToken就会交给当前处理器
        return authentication.isAssignableFrom(UPAuthenticationToken.class);
    }
}

3. 添加用户认证拦截器(LoginAuthenticationFilter)

这里就是数据构造,从request中拿到数据构造成不同的AbstractAuthenticationToken 实体,系统会自动找对应的AuthenticationProvider 进行处理。


import com.lljing.common.util.JsonUtil;
import com.lljing.common.util.ServletUtils;
import com.sinoprof.sinoauth.domain.UPAuthenticationToken;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

import java.io.IOException;
import java.util.Map;

/**
 * <用户认证拦截器>
 * <功能详细描述>
 *
 * @author:JingLonglong
 * @date:2025/9/22
 */
@Slf4j
public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final String AUTHENTICATION_SCHEME = "/oauth/token";

    public LoginAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(request -> AUTHENTICATION_SCHEME.equalsIgnoreCase(request.getRequestURI()) &&
                request.getMethod().equalsIgnoreCase(HttpMethod.POST.name()), authenticationManager);
    }

    /***
     * @desc <获取登录参数,构建不通的请求实体交给不通的认证处理器去处理>
     * <功能详细描述>
     * @param request
     * @param response
     * @date 2025年9月22日 10:20
     * @return	org.springframework.security.core.Authentication
     * @exception
    */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {


        String postBody = ServletUtils.getPostBody(request);

        Map<String, String> collect = JsonUtil.fromJson(postBody, new TypeReference<Map<String, String>>() {});

        //根据请求类型来区分认证实体
        String grantType = collect.get("grant_type");
        UPAuthenticationToken authenticationToken;
        //pathParams放在detail中
        if ("cbc".equalsIgnoreCase(grantType)){
            authenticationToken = new UPAuthenticationToken(null);
            authenticationToken.setAuthenticated(false);
            authenticationToken.setDetails(collect);
            authenticationToken.setUsername(collect.get("username"));
            authenticationToken.setPassword(collect.get("password"));
            return super.getAuthenticationManager().authenticate(authenticationToken);
        }

        throw new AuthenticationServiceException("未经授权的认证方式");
    }
}

4. 注册认证结果处理类(LoginResultHandler)

这里将认证成功和失败处理放在了一起 ,如果逻辑复杂,也可以分开。

认真成功后可以使用自定义的方法来签发Token,调用前面配置的Jwt方法即可 ,这里使用了Oauth Service内置的方法,方便后续拓展。具体实现见:Token签发


import com.lljing.common.api.R;
import com.lljing.common.util.JsonUtil;
import com.sinoprof.sinoauth.constant.LoginConstant;
import com.sinoprof.sinoauth.domain.SecurityUser;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.UUID;

/**
 * <一句话功能简述>
 * <功能详细描述>
 *
 * @author:JingLonglong
 * @date:2025/9/22
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class LoginResultHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {

    private final RegisteredClientRepository registeredClientRepository;
    private final OAuth2AuthorizationService authorizationService;
    private final JwtEncoder jwtEncoder;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {

        SecurityUser details = (SecurityUser) authentication.getDetails();

        // 1. 获取 RegisteredClient
        RegisteredClient registeredClient = registeredClientRepository.findByClientId(LoginConstant.DEFAULT_CLIENT_ID);

        // 2. 构建 AuthorizationBuilder
        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(details.getUsername())
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .attribute(Authentication.class.getName(), authentication);

        // 3. 构建 AccessToken Claims
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .subject(details.getUsername())
                .issuedAt(Instant.now())
                .expiresAt(Instant.now().plus(1, ChronoUnit.HOURS))
                .claim("roles", authentication.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority).toList())
                .claim("tenantId", details.getTenantId())
                .claim("displayName", details.getDisplayName())
                .claim("companyId", details.getCompanyId())
                .claim("companyName", details.getCompanyName())
                .claim("groupId", details.getGroupId())
                .claim("groupName", details.getGroupName())
                .claim("loginName", details.getLoginName())
                .build();

        // 4. 生成 AccessToken (JWT)
        Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(claims));
        OAuth2AccessToken accessToken = new OAuth2AccessToken(
                OAuth2AccessToken.TokenType.BEARER,
                jwt.getTokenValue(),
                jwt.getIssuedAt(),
                jwt.getExpiresAt(),
                registeredClient.getScopes()
        );
        authorizationBuilder.accessToken(accessToken);

        // 5. 生成 RefreshToken
        OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
                UUID.randomUUID().toString(),
                Instant.now(),
                Instant.now().plus(30, ChronoUnit.DAYS)
        );
        authorizationBuilder.refreshToken(refreshToken);

        // 6. 构建并保存 Authorization
        OAuth2Authorization authorization = authorizationBuilder.build();
        authorizationService.save(authorization);

        // 7. 返回 JSON 给客户端
        Map<String, Object> tokenResponse = Map.of(
                "access_token", accessToken.getTokenValue(),
                "token_type", accessToken.getTokenType().getValue(),
                "expires_in", Duration.between(Instant.now(), accessToken.getExpiresAt()).getSeconds(),
                "refresh_token", refreshToken.getTokenValue()
        );

        response.setContentType("application/json");
        JsonUtil.getSerializerMapper().writeValue(response.getOutputStream(), tokenResponse);
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.warn("用户认证失败: {}", exception.getMessage());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");

        response.getWriter().println(JsonUtil.toJSONString(R.failed(exception.getMessage())));
    }
}

5. 注册拦截器到web中(WebSecurityConfig)

这里面做的事情比较多 ,后面还有附加的资源认真的配置。主要做了下列是想

  1. 将所有的认证处理类添加到AuthenticationManager中(authenticationManager方法)。

  2. 添加认证拦截器 LoginAuthenticationFilter filter = new LoginAuthenticationFilter(authenticationManager); .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)

  3. 添加资源验证拦截器(携带Token的时候进入时调用这里).oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.decoder(jwtDecoder)) .authenticationEntryPoint(unauthorizedHandler) // <- 指定自定义入口 )

  4. 设置白名单

  5. 设置统一返回信息处理类

  6. 关闭Session、csrf、httpBase


import com.sinoprof.sinoauth.filter.LoginAuthenticationFilter;
import com.sinoprof.sinoauth.filter.LoginResultHandler;
import com.sinoprof.sinoauth.granter.CustomAccessDeniedHandler;
import com.sinoprof.sinoauth.granter.CustomAuthenticationEntryPoint;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.List;

/***
 * @desc <pringSecurity配置>
 * @date 2025年9月23日 16:58
 * @return	
 * @exception
*/
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig {

    private final List<AuthenticationProvider> authenticationProvider;

    private final LoginResultHandler loginResultHandler;

    private final CustomAuthenticationEntryPoint unauthorizedHandler;

    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    private final JwtDecoder jwtDecoder;

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        LoginAuthenticationFilter filter = new LoginAuthenticationFilter(authenticationManager);
        filter.setAuthenticationSuccessHandler(loginResultHandler);
        filter.setAuthenticationFailureHandler(loginResultHandler);
        return http
                // 基于token,不使用session
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        // 白名单
                        .requestMatchers(
                                "/rsa/publicKey",
                                "/oauth/token",
                                "/oauth/faceCheck",
                                "/oauth/getAuthImage",
                                "/oauth/getLoginOutUrl"
                        ).permitAll()
                        .anyRequest().authenticated()
                )
                .csrf(AbstractHttpConfigurer::disable)
                .cors(Customizer.withDefaults())
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(unauthorizedHandler)
                        .accessDeniedHandler(customAccessDeniedHandler)
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt.decoder(jwtDecoder))
                        .authenticationEntryPoint(unauthorizedHandler) // <- 指定自定义入口
                )
                .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

Token签发

这里使用Oauth内置方法进行签发 ,按照以下步骤 。

1. 配置客户端(AuthorizationServerConfig)


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;

import java.time.Duration;
import java.util.UUID;

@Configuration
public class AuthorizationServerConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端 ID
                .clientId(LoginConstant.DEFAULT_CLIENT_ID)
                
                // 客户端密钥 (我是SPA应用,没有地方放登录和鉴权,这里暂时不需要)
//                .clientSecret("{noop}demo-secret")
                // 验证方式
                .clientAuthenticationMethod("web-client")
                // 授权方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 支持刷新
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 授权范围(后续使用springSecurity的权限认证,这个也可以不配置)
//                .scope("demo.read")
                .tokenSettings(TokenSettings.builder()
                        // Access Token 生命周期
                        .accessTokenTimeToLive(Duration.ofHours(1))
                        // Refresh Token 生命周期
                        .refreshTokenTimeToLive(Duration.ofDays(30))
                        // 是否重复使用 Refresh Token
                        .reuseRefreshTokens(true)
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(client);
    }
}

认证信息存储(RedisOAuth2AuthorizationService)

这里使用Redis,系统提供了数据库存储和内存存储两种方式 ,直接拓展只需要实现OAuth2AuthorizationService即可


import com.lljing.common.redis.service.RedisService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.stereotype.Component;

/**
 * <Token管理实现>
 *
 * @author:JingLonglong
 * @date:2025/9/23
 */
@Component
@RequiredArgsConstructor
public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {

    private static final String AUTH_KEY_PREFIX = "U:TK:%S";

    private final RedisService redisService;
    @Override
    public void save(OAuth2Authorization authorization) {
        redisService.set(String.format(AUTH_KEY_PREFIX, authorization.getId()), authorization.getRefreshToken(), 18000);
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        redisService.del(String.format(AUTH_KEY_PREFIX, authorization.getId()));
    }

    @Override
    public OAuth2Authorization findById(String id) {
        return null;
    }

    @Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
        return null;
    }
}

资源认证实现

在JwtConfig类中已经做了实现 ,这里强调一下 ,需要使用RSA密钥库的公钥进行验证。

/**
     * @desc <JWT令牌解码器配置>
     * @param rsaKey 从JwkConfig中注入的RSAKey
     * @date 2025年9月23日
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(RSAKey rsaKey) {
        // 使用RSAKey中的公钥来创建JWT解码器
        try {
            return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build();
        } catch (JOSEException e) {
            throw new AuthenticationFailedException("无效的请求token");
        }
    }

微服务下其他服务鉴权

微服务下其他服务鉴权逻辑和auth服务的资源认证部分基本类似。只不过公钥通过接口获取。一般只需要在网管中实现认证授权即可,授权通过后分发到其他服务进行请求。

1. auth服务暴露公钥

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * 获取RSA公钥接口
 * Created by wkc on 2021/6/19.
 */
@RestController
@RequiredArgsConstructor
public class KeyPairController {

    private final RSAKey rsaKey;

    @GetMapping("/rsa/publicKey")
    public Map<String, Object> getKey() {
        return new JWKSet(rsaKey.toPublicJWK()).toJSONObject();
    }
}

2. gateway注册认证(GatewaySecurityConfig)

这里使用了LoadBalance通过服务调用避免了部署不通环境导致url需要修改的问题。

并且网关使用的是WebFlux,认证写法也略有不同。


import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancedExchangeFilterFunction;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Collections;

/***
 * @desc <资源认证实现>
 * <通过token进行认证>
 * @date 2025年9月23日 18:52
 * @return	
 * @exception
*/
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class GatewaySecurityConfig {

    private final IgnoreUrlsConfig ignoreUrlsConfig;

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    private String jwkUri;

    /**
     * 配置JwtDecoder,通过认证服务暴露的端点获取JWK Set URI
     * 假设你的认证服务在/oauth/token_key端点返回密钥信息
     */
    @Bean
    public ReactiveJwtDecoder jwtDecoder(ReactiveLoadBalancer.Factory<ServiceInstance> lbFactory) {
        LoadBalancedExchangeFilterFunction lbFunction = new ReactorLoadBalancerExchangeFilterFunction(lbFactory, Collections.emptyList());

        WebClient webClient = WebClient.builder()
                .filter(lbFunction)
                .build();

        // 服务名
        return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUri)
                .webClient(webClient)
                .build();
    }


    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .cors(ServerHttpSecurity.CorsSpec::disable)
                .authorizeExchange(exchanges -> {
                     ignoreUrlsConfig.getUrls().forEach(url -> exchanges.pathMatchers(url).permitAll());
                            exchanges.anyExchange().authenticated();
                })
                .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder))
        );
        return http.build();
    }
}

后端, 高手之路
Java
License:  CC BY 4.0
Share

Further Reading

Sep 30, 2025

微服务之Auth篇

auth服务重要是认证授权,签发jwt token使用。 生成密钥对 # 在项目 src/main/resources 下生成 jwt.jks(演示用,生产用更严格的密码/keystore) keytool -genkeypair \ -alias jwt \ -keyalg RSA \

Sep 6, 2025

SpringBoot3.X-2(缓存Redis/memory)

本文主要是实现缓存的集成,由于是单体项目 ,目前整合了内存缓存和Redis缓存两种,可以通过配置来切换。 引入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-sta

Sep 5, 2025

SpringBoot3.X-1(MP+Druid)

本次基于springboot-3.5.5,先附上文档地址:https://docs.spring.io/spring-boot/reference/data/sql.html 初始化springboot项目 项目创建步骤不做记录 集成mybaits-plus</

OLDER

记一次前端优化(vue2)

NEWER

KubeShpere部署(4.1.2)

Recently Updated

  • KubeShpere部署(4.1.2)
  • 微服务之Auth篇
  • 记一次前端优化(vue2)
  • SpringBoot3.X-2(缓存Redis/memory)
  • SpringBoot3.X-1(MP+Druid)

Trending Tags

Java Docker 前端 中间件 数据库 群晖 unraid

Contents

©2025 十六小站. Some rights reserved.

Using the Halo theme Chirpy