Spring Authorization Server入门 (五) 自定义异常响应配置

前言

在第三章的时候就有提到过响应信息的问题,按照oauth协议,异常信息放在响应头中,响应头的key是WWW-Authenticate,通常来说返回一个JSON字符串可以让开发者更方便的对响应做出一些处理,那么本篇文章就实现一下权限不足与未登录时的自定义处理。

代码实现并添加至配置中

有三种写法实现
1. 实现AuthenticationEntryPoint和AccessDeniedHandler 或他们的子类,并将其实例添加至框架中
自定义AccessDeniedHandler实例
2. 在需要的地方通过一个匿名类来实现自定义处理
匿名类来实现自定义处理
3. 写一个工具方法,通过Method references的方式引用
Method references的方式引用工具方法
三种方式的核心都是实现并重写核心方法,看个人喜好选择对应的写法。
这里本人选择第三种,并根据未登录和权限不足的默认实现将他们整合在一起,实现未登录和权限不足可以通过一个方法处理

在util包下创建SecurityUtils工具类,并实现exceptionHandler公共静态方法

SecurityUtils.java内容如下

package com.example.util;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.StringUtils;

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

/**
 * 认证鉴权工具
 *
 * @author vains
 */
@Slf4j
public class SecurityUtils {

    private SecurityUtils() {
        // 禁止实例化工具类
        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
    }

    /**
     * 认证与鉴权失败回调
     *
     * @param request  当前请求
     * @param response 当前响应
     * @param e        具体的异常信息
     */
    public static void exceptionHandler(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        Map<String, String> parameters = getErrorParameter(request, response, e);
        String wwwAuthenticate = computeWwwAuthenticateHeaderValue(parameters);
        response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
        try {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(JsonUtils.objectCovertToJson(parameters));
            response.getWriter().flush();
        } catch (IOException ex) {
            log.error("写回错误信息失败", e);
        }
    }

    /**
     * 获取异常信息map
     *
     * @param request  当前请求
     * @param response 当前响应
     * @param e        本次异常具体的异常实例
     * @return 异常信息map
     */
    private static Map<String, String> getErrorParameter(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        Map<String, String> parameters = new LinkedHashMap<>();
        if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
            // 权限不足
            parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
            parameters.put("error_description",
                    "The request requires higher privileges than provided by the access token.");
            parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
            response.setStatus(HttpStatus.FORBIDDEN.value());
        }
        if (e instanceof OAuth2AuthenticationException authenticationException) {
            // jwt异常,e.g. jwt超过有效期、jwt无效等
            OAuth2Error error = authenticationException.getError();
            parameters.put("error", error.getErrorCode());
            if (StringUtils.hasText(error.getUri())) {
                parameters.put("error_uri", error.getUri());
            }
            if (StringUtils.hasText(error.getDescription())) {
                parameters.put("error_description", error.getDescription());
            }
            if (error instanceof BearerTokenError bearerTokenError) {
                if (StringUtils.hasText(bearerTokenError.getScope())) {
                    parameters.put("scope", bearerTokenError.getScope());
                }
                response.setStatus(bearerTokenError.getHttpStatus().value());
            }
        }
        if (e instanceof InsufficientAuthenticationException) {
            // 没有携带jwt访问接口,没有客户端认证信息
            parameters.put("error", BearerTokenErrorCodes.INVALID_TOKEN);
            parameters.put("error_description", "Not authorized.");
            parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }
        parameters.put("message", e.getMessage());
        return parameters;
    }

    /**
     * 生成放入请求头的错误信息
     *
     * @param parameters 参数
     * @return 字符串
     */
    public static String computeWwwAuthenticateHeaderValue(Map<String, String> parameters) {
        StringBuilder wwwAuthenticate = new StringBuilder();
        wwwAuthenticate.append("Bearer");
        if (!parameters.isEmpty()) {
            wwwAuthenticate.append(" ");
            int i = 0;
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
                if (i != parameters.size() - 1) {
                    wwwAuthenticate.append(", ");
                }
                i++;
            }
        }
        return wwwAuthenticate.toString();
    }
}

类中引用的工具类JsonUtils.java内容如下

package com.example.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;

/**
 * <p>
 *  JSON与对象互转帮助类
 * </p>
 *
 * @author vains
 * @since 2020-11-10
 */
@Slf4j
public class JsonUtils {

    private JsonUtils() {
        // 禁止实例化工具类
        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
    }

    private final static ObjectMapper MAPPER = new ObjectMapper();

    static {
        // 对象的所有字段全部列入,还是其他的选项,可以忽略null等
        MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
        // 取消默认的时间转换为timeStamp格式
        MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // 设置Date类型的序列化及反序列化格式
        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 忽略空Bean转json的错误
        MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * json字符串转为对象
     * @param json json
     * @param clazz T类的class文件
     * @param <T> 泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, Class<T> clazz) {
        if (json == null || clazz == null) {
            return null;
        }
        try {
            return MAPPER.readValue(json, clazz);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * json字符串转为对象
     * @param json json
     * @param type 对象在Jackson中的类型
     * @param <T> 泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, TypeReference<T> type) {
        if (json == null || type == null) {
            return null;
        }
        try {
            return MAPPER.readValue(json, type);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 将流中的数据转为java对象
     * @param inputStream 输入流
     * @param clazz 类的class
     * @param <T> 泛型, 代表返回参数的类型
     * @return 返回对象 如果参数任意一个为 null则返回null
     */
    public static <T> T covertStreamToObject(InputStream inputStream, Class<T> clazz) {
        if (inputStream == null || clazz == null) {
            return null;
        }
        try {
            return MAPPER.readValue(inputStream, clazz);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * json字符串转为复杂类型List
     * @param json json
     * @param collectionClazz 集合的class
     * @param elementsClazz 集合中泛型的class
     * @param <T> 泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, Class<?> collectionClazz, Class<?> ... elementsClazz) {
        if (json == null || collectionClazz == null || elementsClazz == null) {
            return null;
        }
        try {
            JavaType javaType = MAPPER.getTypeFactory().constructParametricType(collectionClazz, elementsClazz);
            return MAPPER.readValue(json, javaType);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 对象转为json字符串
     * @param o 将要转化的对象
     * @return 返回json字符串
     */
    public static String objectCovertToJson(Object o) {
        if (o == null) {
            return null;
        }
        try {
            return o instanceof String ? (String) o : MAPPER.writeValueAsString(o);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 将对象转为另一个对象
     *      切记,两个对象结构要一致
     *      多用于Object转为具体的对象
     * @param o 将要转化的对象
     * @param collectionClazz 集合的class
     * @param elementsClazz 集合中泛型的class
     * @param <T> 泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static  <T> T objectCovertToObject(Object o, Class<?> collectionClazz, Class<?>... elementsClazz) {
        String json = objectCovertToJson(o);
        return jsonCovertToObject(json, collectionClazz, elementsClazz);
    }

}

添加完这两个类之后在认证相关的过滤器链中将处理异常的实现配置进去。如下

// 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的token
http.oauth2ResourceServer((resourceServer) -> resourceServer
        .jwt(Customizer.withDefaults())
        .accessDeniedHandler(SecurityUtils::exceptionHandler)
        .authenticationEntryPoint(SecurityUtils::exceptionHandler)
);

到这里自定义的处理就配置完成了,为避免介绍的过于抽象而造成各位读者思维混乱,接下来先放一下完整的AuthorizationConfig.java类,并贴一下项目结构图

package com.example.config;

import com.example.authorization.DeviceClientAuthenticationConverter;
import com.example.authorization.DeviceClientAuthenticationProvider;
import com.example.util.SecurityUtils;
import com.nimbusds.jose.jwk.JWKSet;
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.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

/**
 * 认证配置
 * {@link EnableMethodSecurity} 开启全局方法认证,启用JSR250注解支持,启用注解 {@link Secured} 支持,
 * 在Spring Security 6.0版本中将@Configuration注解从@EnableWebSecurity, @EnableMethodSecurity, @EnableGlobalMethodSecurity
 * 和 @EnableGlobalAuthentication 中移除,使用这些注解需手动添加 @Configuration 注解
 * {@link EnableWebSecurity} 注解有两个作用:
 * 1. 加载了WebSecurityConfiguration配置类, 配置安全认证策略。
 * 2. 加载了AuthenticationConfiguration, 配置了认证信息。
 *
 * @author vains
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {

    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    /**
     * 配置端点的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
                                                                      RegisteredClientRepository registeredClientRepository,
                                                                      AuthorizationServerSettings authorizationServerSettings) throws Exception {
        // 配置默认的设置,忽略认证端点的csrf校验
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        // 新建设备码converter和provider
        DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
                new DeviceClientAuthenticationConverter(
                        authorizationServerSettings.getDeviceAuthorizationEndpoint());
        DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
                new DeviceClientAuthenticationProvider(registeredClientRepository);


        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 开启OpenID Connect 1.0协议相关端点
                .oidc(Customizer.withDefaults())
                // 设置自定义用户确认授权页
                .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
                // 设置设备码用户验证url(自定义用户验证页)
                .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
                        deviceAuthorizationEndpoint.verificationUri("/activate")
                )
                // 设置验证设备码用户确认页面
                .deviceVerificationEndpoint(deviceVerificationEndpoint ->
                        deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
                )
                .clientAuthentication(clientAuthentication ->
                        // 客户端认证添加设备码的converter和provider
                        clientAuthentication
                                .authenticationConverter(deviceClientAuthenticationConverter)
                                .authenticationProvider(deviceClientAuthenticationProvider)
                );
        http
                // 当未登录时访问认证端点时重定向至login页面
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // 处理使用access token访问用户信息端点和客户端注册端点
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }

    /**
     * 配置认证相关的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                        // 放行静态资源
                        .requestMatchers("/assets/**", "/webjars/**", "/login").permitAll()
                        .anyRequest().authenticated()
                )
                // 指定登录页面
                .formLogin(formLogin ->
                        formLogin.loginPage("/login")
                );
        // 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的token
        http.oauth2ResourceServer((resourceServer) -> resourceServer
                .jwt(Customizer.withDefaults())
                .accessDeniedHandler(SecurityUtils::exceptionHandler)
                .authenticationEntryPoint(SecurityUtils::exceptionHandler)
        );

        return http.build();
    }

    /**
     * 配置密码解析器,使用BCrypt的方式对密码进行加密和验证
     *
     * @return BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置客户端Repository
     *
     * @param jdbcTemplate    db 数据源信息
     * @param passwordEncoder 密码解析器
     * @return 基于数据库的repository
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id
                .clientId("messaging-client")
                // 客户端秘钥,使用密码解析器加密
                .clientSecret(passwordEncoder.encode("123456"))
                // 客户端认证方式,基于请求头的认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 配置资源服务器使用该客户端获取授权时支持的方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
                .redirectUri("https://www.baidu.com")
                // 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                // 客户端设置,设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        // 基于db存储客户端,还有一个基于内存的实现 InMemoryRegisteredClientRepository
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        // 初始化客户端
        RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
        if (repositoryByClientId == null) {
            registeredClientRepository.save(registeredClient);
        }
        // 设备码授权客户端
        RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("device-message-client")
                // 公共客户端
                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                // 设备码授权
                .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                .build();
        RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());
        if (byClientId == null) {
            registeredClientRepository.save(deviceClient);
        }

        // PKCE客户端
        RegisteredClient pkceClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("pkce-message-client")
                // 公共客户端
                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                // 授权码模式,因为是扩展授权码流程,所以流程还是授权码的流程,改变的只是参数
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
                .clientSettings(ClientSettings.builder().requireProofKey(Boolean.TRUE).build())
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                .build();
        RegisteredClient findPkceClient = registeredClientRepository.findByClientId(pkceClient.getClientId());
        if (findPkceClient == null) {
            registeredClientRepository.save(pkceClient);
        }
        return registeredClientRepository;
    }

    /**
     * 配置基于db的oauth2的授权管理服务
     *
     * @param jdbcTemplate               db数据源信息
     * @param registeredClientRepository 上边注入的客户端repository
     * @return JdbcOAuth2AuthorizationService
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        // 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置基于db的授权确认管理服务
     *
     * @param jdbcTemplate               db数据源信息
     * @param registeredClientRepository 客户端repository
     * @return JdbcOAuth2AuthorizationConsentService
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        // 基于db的授权确认管理服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationConsentService
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
     *
     * @return JWKSource
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * 生成rsa密钥对,提供给jwk
     *
     * @return 密钥对
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * 配置jwt解析器
     *
     * @param jwkSource jwk源
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 添加认证服务器配置,设置jwt签发者、默认端点请求地址等
     *
     * @return AuthorizationServerSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    /**
     * 先暂时配置一个基于内存的用户,框架在用户认证时会默认调用
     * {@link UserDetailsService#loadUserByUsername(String)} 方法根据
     * 账号查询用户信息,一般是重写该方法实现自己的逻辑
     *
     * @param passwordEncoder 密码解析器
     * @return UserDetailsService
     */
    @Bean
    public UserDetailsService users(PasswordEncoder passwordEncoder) {
        UserDetails user = User.withUsername("admin")
                .password(passwordEncoder.encode("123456"))
                .roles("admin", "normal", "unAuthentication")
                .authorities("app", "web", "/test2", "/test3")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

}

项目结构图
项目结构图

测试

配置完成后就带大家测试下看看。

不携带token访问接口

未携带token访问
从图中可以看出接口响应了JSON字符串,并且http状态码是401未登录。

携带异常token访问

携带异常token访问示例
响应了JSON字符串并且给出错误原因

请求接口权限不足

@GetMapping("/test02")
@PreAuthorize("hasAuthority('SCOPE_message.write')")
public String test02() {
    return "test02";
}

测试接口中添加一个需要message.write写的权限的接口
申请一个scope为message.read的access token分别访问这两个接口,查看对比

访问test02接口

请求test02接口示例
响应json并正确提示异常原因,响应状态码为403权限不足。

访问test01

请求test01示例
可以看到通过鉴权并正常响应。

浏览器中直接访问test01接口

浏览器会直接重定向至登录页面
重定向至登录
登录后响应权限不足json
权限不足页面
其实这里完全可以模仿未登录处理配置,在http.exceptionHandling中配置defaultAccessDeniedHandlerFor,配置一个只在浏览器请求并且权限不足时跳转的页面。这里大家自行扩展就好。
到此编码与测试部分结束,剩下的就是理论了。

默认异常响应介绍

未登录 AuthenticationEntryPoint

AuthenticationEntryPoint源码
通过源码可以看出,如果要定义自己的处理则需要实现该接口,并重写commence方法,ExceptionTranslationFilter中会调用commence方法来对未登录的请求进行处理。

LoginUrlAuthenticationEntryPoint

通过右侧继承类图可以看到框架有一些默认的实现,首先是LoginUrlAuthenticationEntryPoint,这个是认证服务器的默认实现,在配置认证服务器时什么自定义处理都不加的情况下,在浏览器访问一个访问受限的接口时会通过该类跳转至登录接口,可能有读者会觉得这个类比较眼熟,因为在第二篇文章中配置基础的认证服务器时通过http.exceptionHandling添加了该类的配置,如下

http
    // 当未登录时访问认证端点时重定向至login页面
    .exceptionHandling((exceptions) -> exceptions
        .defaultAuthenticationEntryPointFor(
            new LoginUrlAuthenticationEntryPoint("/login"),
            new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
        )
    )

这段代码的意思是所有来自页面的请求都由LoginUrlAuthenticationEntryPoint来处理。

BearerTokenAuthenticationEntryPoint

BearerTokenAuthenticationEntryPoint是资源服务器默认的未登录异常(未携带token),还是第二章的基础认证服务器配置中,在认证相关的过滤器链中添加了如下配置

// 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的token
http.oauth2ResourceServer((resourceServer) -> resourceServer
        .jwt(Customizer.withDefaults()));

上边的LoginUrlAuthenticationEntryPoint是处理所有来自页面的请求,这里就是处理所有非页面的请求。稍后后边会讲到为什么。

权限不足 AccessDeniedHandler

 AccessDeniedHandler源码
AccessDeniedHandler跟AuthenticationEntryPoint差不多的,框架都有提供默认实现,resource server的BearerTokenAccessDeniedHandler和Spring Security的AccessDeniedHandlerImpl,要实现自定义内容时同样是实现接口并重写handle方法。

BearerTokenAccessDeniedHandler

BearerTokenAccessDeniedHandler源码
访问资源服务器时如果获取授权的scope权限不足时会通过该类去处理,该类会将http响应码设置为403,并根据RFC 6750 Section的提案将错误信息放入响应头中。

AccessDeniedHandlerImpl

AccessDeniedHandlerImpl源码
默认的权限不足处理类根据源码来看就是如果配置类权限不足页面则会跳转至权限不足页面,如果没有配置则设置http响应码为403返回。

框架怎么在我未登录的情况下知道是重定向至登录页面还是返回一个json提醒我未登录

未登录和权限不足父接口中都提到了在ExceptionTranslationFilter中被调用,就在该过滤器中看一下逻辑,打个端点,看看请求经过的过程,发现异常处理是第147行的handleSpringSecurityException方法处理。
ExceptionTranslationFilter源码
追踪实现后会在219行看到具体的未登录处理,打个断点看看是怎么处理的
具体的未登录处理
首先先在浏览器发起一个请求
断点进入实现发现是一个EntryPoint委托类,内部实现逻辑是请求类型来提供不同的entryPoint来处理
断点进入委托类
接下来一起看一下this.entryPoints中的结构
this.entryPoint结构示例
可以看到会根据请求的mediaType判断,通过浏览器发出的请求肯定会携带accept : text/html的请求头,所以可以根据这个来判断是否为浏览器发出的请求。

this.entryPoints是从构造方法中初始化的,为了更好理解,我们看一下实例化这个委托类的地方。
实例化委托类
可以看到在ExceptionHandlingConfigurer中实例化委托类并且传入了this.defaultEntryPointMappings。再来看下this.defaultEntryPointMappings又是在什么地方赋值的。
给this.defaultEntryPointMappings赋值的地方
看到这个方法相信各位读者应该有些眼熟,就是前边设置跳转至登录页的地方,并限制只能浏览器的请求可会转发至登录页调用defaultAuthenticationEntryPointFor
到这里就知道了在浏览器发起请求为什么会跳转至登录页面而不是像postman中一样返回一个JSON,继续追踪,看看框架默认的设置
默认设置
可以看到框架添加了一个针对非浏览器发出的请求的处理,继续追踪看看this.authenticationEntryPoint是从哪里来的
方法调用地方
可以看到,在认证相关的过滤器链中调用了该方法,重新设置了this.authenticationEntryPoint;至此也明白了为什么在postman中请求返回json。

总结

本篇文章实现自定义的异常响应配置,并通过断点反查了一下配置生效的过程,粗略的说明了一下security对于异常处理的逻辑。

文章内容比较杂,写的比较乱,如果有发现什么问题请在评论区指出,谢谢。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
### 回答1: 关于spring-authorization-server定义认证配置,我可以回答您的问题。 Spring Authorization ServerSpring Security 的一个子项目,它提供了 OAuth 2.0 和 OpenID Connect(OIDC)的认证和授权功能。在使用 Spring Authorization Server 进行自定义认证配置时,可以通过创建实现特定接口的 bean 或者在配置文件中指定相关配置来完成。 具体地说,可以通过实现 AuthorizationServerConfigurer 接口来进行自定义认证配置。该接口定义了一系列方法,例如: - configure(ClientDetailsServiceConfigurer clients):配置客户端详情服务。 - configure(AuthorizationServerEndpointsConfigurer endpoints):配置授权端点的URL和令牌服务。 - configure(AuthorizationServerSecurityConfigurer security):配置授权服务器的安全性。 通过实现这些方法并设置相关属性,可以对 Spring Authorization Server 进行自定义认证配置。例如,可以设置支持的授权类型、客户端详情信息、令牌存储方式、用户认证方式等等。 除了实现接口进行自定义配置外,还可以通过在配置文件中指定相关配置来完成。例如,在 application.yml 文件中添加以下配置可以设置支持的授权类型: ``` spring: authorization: server: token: issuer-uri: https://example.com/ access-token: jwt: signature-algorithm: RS256 jwk-set-uri: https://example.com/oauth2/keys supported-grant-types: authorization_code,client_credentials,password,refresh_token ``` 以上是关于spring-authorization-server定义认证配置的回答,希望能对您有所帮助。 ### 回答2: spring-authorization-serverSpring框架的一个模块,用于构建带有自定义认证配置的授权服务器。 在使用spring-authorization-server时,我们可以通过自定义认证配置来对授权服务器进行个性化的定制。具体步骤如下: 1. 创建自定义认证配置类:我们可以创建一个自定义配置类,继承自AuthorizationServerConfigurerAdapter,并在类上加上@Configuration注解。在这个类中,我们可以重写configure方法,对授权服务器的认证配置进行定制。 2. 配置认证服务:在configure方法中,我们可以使用AuthorizationServerEndpointsConfigurer对象的方法对认证服务进行配置。例如,可以使用tokenStore方法设置token的存储方式,使用authenticationManager方法设置认证管理器,使用userDetailsService方法设置用户详细信息服务等。 3. 配置客户端信息:我们可以使用AuthorizationServerEndpointsConfigurer对象的方法对客户端信息进行配置。例如,可以使用inMemory方法将客户端信息存储在内存中,使用withClient方法设置客户端的clientId和clientSecret等。 4. 配置授权的方式:我们可以使用AuthorizationServerSecurityConfigurer对象的方法来配置授权的方式。例如,可以使用tokenKeyAccess方法设置token的访问权限,使用checkTokenAccess方法设置检查token的权限等。 通过以上步骤,我们可以根据业务需要对授权服务器的认证配置进行自定义。可根据具体的需求来选择存储方式、认证方式、访问权限等,从而实现对授权服务器的个性化定制。使用spring-authorization-server的自定义认证配置,可以实现更灵活、更安全的授权服务器构建。 ### 回答3: spring-authorization-server是一个基于Spring框架的授权服务器,用于处理认证和授权的任务。它提供了一种自定义认证配置的方式来满足应用程序的特定需求。 首先,我们可以通过创建一个实现了AuthorizationServerConfigurer接口的配置类来实现自定义认证配置。该接口提供了一些方法,可以用于配置授权服务器的行为。 通过重写configure(ClientDetailsServiceConfigurer clients)方法,我们可以定义客户端的认证信息。我们可以指定客户端的ID和秘钥,并设置各种认证方式,如基于密码、授权码、简化模式等。这些设置将决定客户端可以使用哪种方式进行认证和授权。 其次,我们可以通过重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法来配置授权服务器的终端。我们可以指定认证管理器、用户详情服务和令牌存储等终端的相关信息。我们还可以自定义令牌生成方式、令牌验证方式以及访问令牌的有效期等。 另外,我们还可以通过重写configure(AuthorizationServerSecurityConfigurer security)方法来配置授权服务器的安全性。我们可以设置授权服务器的访问权限,例如要求授权请求必须经过身份验证等。 除了以上方法,我们还可以结合其他Spring框架提供的相关组件和功能来实现自定义认证配置。例如,可以使用自定义的UserDetailsService来配置用户的认证信息,或者使用自定义的TokenStore来实现令牌的自定义存储策略。 总之,通过使用spring-authorization-server,我们可以根据应用程序的需求来灵活地配置认证和授权的行为。通过自定义认证配置,我们可以定制化地满足各种认证需求,提高应用程序的安全性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天玺-vains

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值