SpringOAuth2Server 自定义授权码认证,登录和授权码混合

java 复制代码
package com.kongjs.oauth.component.authentication;

import com.kongjs.common.session.authentication.RestAuthentication;
import com.kongjs.oauth.component.authentication.dto.LoginInfo;
import com.kongjs.oauth.component.password.SMO2FRawEncryptor;
import com.kongjs.oauth.exception.AuthParamException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.util.*;

//@Component
public class RestAuthenticationConverter implements AuthenticationConverter {

    private final JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter = new JacksonJsonHttpMessageConverter();

    @Resource
    private SMO2FRawEncryptor smo2FRawEncryptor;

    @Override
    public Authentication convert(HttpServletRequest request) {
        try {
            MultiValueMap<String, String> param = param(request);
            Map<String, Object> additional = additional(param);
            ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
            LoginInfo loginInfo = (LoginInfo) jacksonJsonHttpMessageConverter.read(LoginInfo.class, httpRequest);
            if (!StringUtils.hasText(loginInfo.getTenant())) {
                throw new AuthParamException("error.auth.param.tenant");
            }
            if (!StringUtils.hasText(loginInfo.getUsername())) {
                throw new AuthParamException("error.auth.param.username");
            }
            if (!StringUtils.hasText(loginInfo.getPassword())) {
                throw new AuthParamException("error.auth.param.password");
            }
            try {
                String decryptPassword = smo2FRawEncryptor.decrypt(loginInfo.getPassword());
                loginInfo.setPassword(decryptPassword);
            } catch (Exception e) {
                throw new AuthParamException("error.auth.param.password.decrypt");
            }
            if (!StringUtils.hasText(loginInfo.getPassword())) {
                throw new AuthParamException("error.auth.param.password.decrypt");
            }
            //loginInfo.setDeviceType(deviceType);
            //loginInfo.setDeviceName(deviceName);
            loginInfo.setParams(param);
            loginInfo.setAdditional(additional);
            return RestAuthentication.unauthenticated(loginInfo, null);
        } catch (AuthParamException e) {
            throw e;
        } catch (Exception e) {
            throw new AuthParamException("error.auth.param.unknown");
        }
    }

    private MultiValueMap<String, String> param(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameterMap.forEach((key, values) -> {
            for (String value : values) {
                parameters.add(key, value);
            }
        });

        // response_type (REQUIRED)
        String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
        if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
        } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
            throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
        }

        // client_id (REQUIRED)
        String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
        if (!StringUtils.hasText(clientId) || parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
        }

        // redirect_uri (OPTIONAL)
        String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
        if (StringUtils.hasText(redirectUri) && parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
        }

        // scope (OPTIONAL)
        Set<String> scopes = null;
        String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
        if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
        }
        if (StringUtils.hasText(scope)) {
            scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
        }

        // state (RECOMMENDED)
        String state = parameters.getFirst(OAuth2ParameterNames.STATE);
        if (StringUtils.hasText(state) && parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
        }

        // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
        String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
        if (StringUtils.hasText(codeChallenge) && parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE);
        }

        // code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
        String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
        if (StringUtils.hasText(codeChallengeMethod)
                && parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
            throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD);
        }

        // prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
        if (!CollectionUtils.isEmpty(scopes) && scopes.contains(OidcScopes.OPENID)) {
            String prompt = parameters.getFirst("prompt");
            if (StringUtils.hasText(prompt) && parameters.get("prompt").size() != 1) {
                throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt");
            }
        }
        return parameters;
    }

    private static Map<String, Object> additional(MultiValueMap<String, String> parameters) {
        Map<String, Object> additionalParameters = new HashMap<>();
        parameters.forEach((key, value) -> {
            if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) && !key.equals(OAuth2ParameterNames.CLIENT_ID)
                    && !key.equals(OAuth2ParameterNames.REDIRECT_URI) && !key.equals(OAuth2ParameterNames.SCOPE)
                    && !key.equals(OAuth2ParameterNames.STATE)) {
                additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
            }
        });
        return additionalParameters;
    }

    private void throwError(String errorCode, String paramName) {
        throw new AuthParamException(paramName);
    }

}
java 复制代码
package com.kongjs.oauth.component.authentication;


import com.kongjs.common.session.authentication.RestAuthentication;
import com.kongjs.common.session.dto.AccountInfo;
import com.kongjs.oauth.component.authentication.dto.LoginInfo;
import com.kongjs.oauth.component.password.SMO2FPasswordEncoder;
import com.kongjs.oauth.exception.AuthParamException;
import com.kongjs.system.entity.SystemUser;
import com.kongjs.system.service.SystemUserService;
import jakarta.annotation.Resource;
import org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerProperties;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;


//@Component
public class RestAuthenticationManager implements AuthenticationManager {

    @Resource
    private OAuth2AuthorizationServerProperties oAuth2AuthorizationServerProperties;
    @Resource
    private SMO2FPasswordEncoder smo2FPasswordEncoder;
    @Resource
    private SystemUserService systemUserService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!(authentication instanceof RestAuthentication)) {
            return authentication;
        }
        LoginInfo loginInfo = (LoginInfo) authentication.getPrincipal();
        if (loginInfo == null) {
            throw new AuthParamException("error.auth.param.user");
        }
        SystemUser user = systemUserService.getSystemUserByTenantCodeAndUserName(loginInfo.getTenant(), loginInfo.getUsername());
        if (user == null) {
            throw new AuthParamException("error.auth.param.user");
        }
        if (!smo2FPasswordEncoder.matches(loginInfo.getPassword(), user.getPassword())) {
            throw new AuthParamException("error.auth.param.password.matches");
        }
        OAuth2AuthorizationServerProperties.Endpoint endpoint = oAuth2AuthorizationServerProperties.getEndpoint();
        String authorizationEndpoint = endpoint.getAuthorizationUri();
        String redirectUri = UriComponentsBuilder.fromUriString(authorizationEndpoint).queryParams(loginInfo.getParams()).build().toString();
        AccountInfo accountInfo = new AccountInfo();
        accountInfo.setLoginId(user.getId());
        accountInfo.setTenantId(user.getTenantId());
        accountInfo.setRedirectUri(redirectUri);
        return RestAuthentication.authenticated(accountInfo, null, null);
    }

}
java 复制代码
package com.kongjs.oauth.component.authentication.handler;

import com.kongjs.common.api.response.ApiResponse;

import com.kongjs.common.session.authentication.RestAuthentication;
import com.kongjs.common.session.dto.AccountInfo;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import tools.jackson.databind.json.JsonMapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

@Component
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private Consumer<OAuth2User> oauth2UserHandler = (user) -> {
    };

    private Consumer<OidcUser> oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user);

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Resource
    private JsonMapper jsonMapper;

    public Consumer<OAuth2User> getOauth2UserHandler() {
        return oauth2UserHandler;
    }

    public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
        this.oauth2UserHandler = oauth2UserHandler;
    }

    public Consumer<OidcUser> getOidcUserHandler() {
        return oidcUserHandler;
    }

    public void setOidcUserHandler(Consumer<OidcUser> oidcUserHandler) {
        this.oidcUserHandler = oidcUserHandler;
    }

    public RedirectStrategy getRedirectStrategy() {
        return redirectStrategy;
    }

    public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
        this.redirectStrategy = redirectStrategy;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        HttpSession session = request.getSession();
        /*
         * if (authentication instanceof OAuth2AuthenticationToken) {
         * if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
         * this.oidcUserHandler.accept(oidcUser);
         * } else if (authentication.getPrincipal() instanceof OAuth2User oauth2User) {
         * this.oauth2UserHandler.accept(oauth2User);
         * }
         * }
         */
        // http://localhost:3000/login?client_id=oidc-client&client_secret=secret&redirect_uri=http://127.0.0.1:3000/login/oauth/code&response_type=code&scope=openid+profile&state=xyzABC123&nonce=defGHI456&code_challenge=YHI7Jk3hF8sW3qL2pQ9mN6rT5vX8zB1cF4dG7jK2lM9nP0sR3uV6wY9zA4&code_challenge_method=S256
        Map<String, Object> map = new HashMap<>();
        if (authentication instanceof RestAuthentication restAuthentication) {
            AccountInfo accountInfo = (AccountInfo) restAuthentication.getPrincipal();
            if (accountInfo != null && StringUtils.hasText(accountInfo.getRedirectUri())) {
                map.put("redirectUri", accountInfo.getRedirectUri());
            }
        }
        map.put("sessionId", session.getId());
        map.put("principal", authentication.getPrincipal());
        ApiResponse apiResponse = ApiResponse.success(map);
        response.setStatus(200);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8);
        jsonMapper.writeValue(response.getOutputStream(), apiResponse);
    }
}
java 复制代码
package com.kongjs.oauth.component.authentication.handler;

import com.kongjs.common.api.response.ApiResponse;
import com.kongjs.oauth.exception.AuthParamException;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import tools.jackson.databind.json.JsonMapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Resource
    private JsonMapper jsonMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String detailMessage = "";
        if (exception instanceof AuthParamException AuthParamException) {
            detailMessage = AuthParamException.getMessage();
        }
        ApiResponse apiResponse = ApiResponse.notAuthentication();
        response.setStatus(200);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8);
        jsonMapper.writeValue(response.getOutputStream(), apiResponse);
    }
}
java 复制代码
package com.kongjs.oauth.component.authentication.filter;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;

public class RestLoginFilter extends AbstractAuthenticationProcessingFilter {
    public RestLoginFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
        super(requiresAuthenticationRequestMatcher, authenticationManager);
    }
}
java 复制代码
package com.kongjs.oauth.config.security;

import com.kongjs.oauth.component.authentication.RestAuthenticationConverter;
import com.kongjs.oauth.component.authentication.RestAuthenticationManager;
import com.kongjs.oauth.component.authentication.filter.RestLoginFilter;
import com.kongjs.oauth.component.authentication.filter.SecurityFilter;
import com.kongjs.oauth.component.authentication.handler.RestAuthenticationFailureHandler;
import com.kongjs.oauth.component.authentication.handler.RestAuthenticationSuccessHandler;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Map;

//@Configuration(proxyBeanMethods = false)
public class RestLoginConfig {
    private static final Logger log = LoggerFactory.getLogger(RestLoginConfig.class);
    private static final RequestMatcher DEFAULT_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults()
            .matcher(HttpMethod.POST, "/rest/login");

    @Resource
    private WebSecurityProperties webSecurityProperties;
    @Resource
    private RestAuthenticationConverter restAuthenticationConverter;
    @Resource
    private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
    @Resource
    private RestAuthenticationFailureHandler restAuthenticationFailureHandler;

    @Bean
    public RestLoginFilter restLoginFilter(RestAuthenticationManager restAuthenticationManager) {
        String restLoginApi = webSecurityProperties.getRestLoginApi();
        PathPatternRequestMatcher requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,
                restLoginApi);
        RestLoginFilter restLoginFilter = new RestLoginFilter(requestMatcher, restAuthenticationManager);
        restLoginFilter.setAuthenticationConverter(restAuthenticationConverter);
        restLoginFilter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
        restLoginFilter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);
        restLoginFilter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
        return restLoginFilter;
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        for (Map.Entry<String, CorsPathProperties> entry : webSecurityProperties.getCorsMappings().entrySet()) {
            CorsPathProperties corsPathProperties = entry.getValue();
            log.info("cors: path: {} config: {}", entry.getKey(), corsPathProperties);
            source.registerCorsConfiguration(entry.getKey(), corsPathProperties.toCorsConfiguration());
        }
        return source;
    }


}
相关推荐
一 乐2 小时前
饮食营养信息|基于springboot + vue饮食营养管理信息平台系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·饮食营养管理信息系统
Devin~Y3 小时前
大厂Java面试实战:Spring Boot/WebFlux、Redis+Kafka、K8s可观测性与Spring AI RAG/Agent三轮连环问
java·spring boot·redis·kafka·kubernetes·resilience4j·spring webflux
悟空码字3 小时前
别再重复造轮子了!SpringBoot对接第三方系统模板,拿来即用
java·spring boot·后端
indexsunny4 小时前
互联网大厂Java求职面试实战:Spring Boot与微服务架构解析
java·spring boot·redis·kafka·spring security·flyway·microservices
我叫张土豆4 小时前
让 AI 学会用工具:基于 LangChain4j 的 Skills Agent 全栈落地实战
人工智能·spring boot
我登哥MVP4 小时前
【SpringMVC笔记】 - 2 - @RequestMapping
java·spring boot·spring·servlet·tomcat·intellij-idea·springmvc
常利兵4 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
spring boot·后端·websocket
希望永不加班5 小时前
SpringBoot 依赖管理:BOM 与版本控制
java·spring boot·后端·spring
勿忘,瞬间5 小时前
Spring Boot
java·数据库·spring boot