Authorized server version:

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-oauth2-authorization-server</artifactId>
   <version>0.2.0</version>
</dependency>
Copy the code

👉 Spring authorization server GitHub source code there are several cases more suitable for use

Using Spring-Security-OAuth2-authorization-server with SSO SSO requires a deep understanding of SpringSecurity and OAuth2.

The single point of configuration for SpringSecurity is limited, but it’s worth mentioning

http.authorizeRequests().antMatchers("/login/**").permitAll()
Copy the code

And can’t be

http.authorizeRequests().antMatchers("/login").permitAll()
Copy the code

or

http.formLogin().permitAll()
Copy the code

Because spring-security-oauth2-authorization-server redirects oauth2 standard using various savedRequest, using HttpSessionRequestCache, and then redirects after completion. Since security does a saveRequest on any url that is not publicly accessible, you must release /login/** instead of /login, otherwise the error page /login? Errors are also cached in the savedRequest, causing users to log in repeatedly if they enter the wrong password, or override oAuth2’s redirection callback

RememberrememberrememberrememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRememberRemember

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.formLogin(withDefaults()).logout().and().cors();
        return http.build();
    }
Copy the code

The OIDC login page is redirected to the default Security login page. The user login page is configured on the default Security page.

A single point to log out

Focus on logout, OidcProviderConfigurationEndpointFilter registers a standard protocol of /. Well – known/openid – configuration endpoint, However, the metadata returned by this endpoint of the Spring authorization server does not include end_session_endpoint information, so the client needs to customize the logout operation. If the client need a single point to logout, you reference org. Springframework. Security. Oauth2. Client. Oidc. Web. Logout. Achieve a OidcClientInitiatedLogoutSuccessHandler himself, Change the endSessionEndpoint method (either lazy and have the front end send two logout requests (laughs), or use WebClinet internally to send a request to the single point server to prevent users from reconnecting).

    private URI endSessionEndpoint(ClientRegistration clientRegistration) {
        if(clientRegistration ! =null) {
            ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
            Object endSessionEndpoint = providerDetails.getConfigurationMetadata().get("issuer");// Url of the OIDC authentication endpoint
            if(endSessionEndpoint ! =null) {
                // Assemble your own single sign-off request address, which can be inconsistent with SSO's normal sign-off address
                return URI.create(endSessionEndpoint + "/logout"); }}return null;
    }
Copy the code

Then register successHandler in the client’s Security configuration

.logout(logout ->
        logout.logoutSuccessHandler(mySsoLogoutSuccessHandler))
)
Copy the code

Then on the single point server side, if CSRF is not enabled LogoutFilter will block all requests, if CSRF is enabled LogoutFilter will only block POSTS, and single signouts initiated by the client are actually redirected requests by the browser (if the signout request is made using Ajax the browser will only make the request but will not redirect the page, Need a page refresh), and the request contains an authorization filter that JWT can pass through SpringSecurity

If CSRF is enabled, an additional configuration is required: the Handler field of the LogoutFilter is retrieved by reflection and used to perform the logout operation manually

import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.util.Assert;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

/** * Manually perform the logout, which is used to perform the user single signout Service.@linkLogoutFilter} the same logout operation (clear cache, clear cookie, clear session, etc., preferably consistent with LogoutFilter behavior) */
@Slf4j
@Getter
public class SsoLogoutService {
    private LogoutHandler handler;
    private LogoutSuccessHandler logoutSuccessHandler;
    private LogoutFilter logoutFilter;
    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    public void setLogoutFilter(LogoutFilter logoutFilter) {
        Assert.notNull(logoutFilter, "logoutFilter cannot be null");
        this.logoutFilter = logoutFilter;
        this.handler = getLogoutHandler(logoutFilter);
        this.logoutSuccessHandler = getLogoutSuccessHandler(logoutFilter);
    }
    If the "post_logout_redirect_uri" parameter is used, the link specified by this parameter will be jumped
    public void logoutAndRedirect(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Assert.notNull(logoutFilter, "logoutFilter cannot be null");
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (log.isDebugEnabled()) {
            log.debug(LogMessage.format("Logging out [%s]", auth).toString());
        }
        this.handler.logout(request, response, auth);
        String redirectUri = request.getParameter("post_logout_redirect_uri");
        if(redirectUri! =null){
            redirectStrategy.sendRedirect(request,response,redirectUri);
            return;
        }
        this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
    }
    /** * returns json */
    public void logout(HttpServletRequest request, HttpServletResponse response){
        Assert.notNull(logoutFilter, "logoutFilter cannot be null");
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (log.isDebugEnabled()) {
            log.debug(LogMessage.format("Logging out [%s]", auth).toString());
        }
        this.handler.logout(request, response, auth);
    }

    @SneakyThrows
    private static LogoutHandler getLogoutHandler(LogoutFilter logoutFilter) {
        Field field = logoutFilter.getClass().getDeclaredField("handler");
        field.setAccessible(true);
        return (LogoutHandler) field.get(logoutFilter);
    }

    @SneakyThrows
    private static LogoutSuccessHandler getLogoutSuccessHandler(LogoutFilter logoutFilter) {
        Field field = logoutFilter.getClass().getDeclaredField("logoutSuccessHandler");
        field.setAccessible(true);
        return(LogoutSuccessHandler) field.get(logoutFilter); }}Copy the code

Registering as a bean requires the following Settings in a single point server’s normal Security configuration:

@Autowired
SsoLogoutService ssoLogoutService
/ /...
.logout(e->e.withObjectPostProcessor(new ObjectPostProcessor<LogoutFilter>(){
    @Override
    public <O extends LogoutFilter> O postProcess(O logoutFilter) {
        // Use postProcess to get the built-in LogoutFilter object, Source in the org. Springframework. Security. Config. The annotation. Web. Configurers. LogoutConfigurer# createLogoutFilter
        ssoLogoutService.setLogoutFilter(logoutFilter);
        returnlogoutFilter; }}))Copy the code

Then you need a logout controller, because any redirected connection initiated by the client will reach the Controller layer if it is not caught by LogoutFilter (and therefore logged out)

@GetMapping("/logout")
@ResponseBody
public Result<String> logout(HttpServletRequest request, HttpServletResponse response
        ,@RequestParam(required = false) String logoutReturnMode) throws ServletException, IOException {
    // Return JSON if it takes a parameter that specifies to return JSON, otherwise go to the default redirect path
    if("JSON".equalsIgnoreCase(logoutReturnMode)){
        ssoLogoutService.logout(request, response);
    }else {
        ssoLogoutService.logoutAndRedirect(request, response);
    }
    return Result.ok("Logout successful"); //Result is my own return class
}
Copy the code

Passing user data

If you want the client to share user data through a single point, such as sharing a custom UserEntity implementation class for UserDetails, but the client logs in through Oauth2, SecurityContextHolder. GetContext (). GetAuthentication () getPrincipal () is not populated UserDetails subclass but OidcUser subclass, You need to implement a custom OidcUser to assemble the UserEntity. You can pass UserDetails in the form of a resource server, but if you need to load the UserEntity, especially the authorities, at the client login authentication stage, You need to implement an OidcUserService to send out HTTP request loading, and you can’t use the normal Oauth2 request process of the resource server, but take OIDC’s AccessToken to find the single point of exchange resources, otherwise you will re-jump to the login of the resource server halfway. The UserEntity cannot be serialized directly by Jackson JSON, otherwise either the authorities will not serialize, or SpringSecurity will not be able to load the UserEntity and will need to customize a UserEntityDTO for transmission

public class SsoOidcUserService extends OidcUserService {
    private WebClient webClient;
    public SsoOidcUserService(OAuth2AuthorizedClientManager authorizedClientManager){
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    webClient = WebClient.builder()
                .apply(oauth2Client.oauth2Configuration()).build();
    }
    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = super.loadUser(userRequest);
        UserEntity userEntity = getSsoUserEntity(new OAuth2AuthorizedClient(userRequest.getClientRegistration(),oidcUser.getName(),userRequest.getAccessToken()));
        //....
    }
    public UserEntity getSsoUserEntity(OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        UserEntity entity = null;
        try {
            entity = UserEntityDTO.toUserEntity(webClient
                    .get()
                    .uri(oAuth2AuthorizedClient.getClientRegistration().getProviderDetails().getIssuerUri()+"/ Your User REST request endpoint")
                    .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(oAuth2AuthorizedClient))
                    .retrieve()
                    .bodyToMono(UserEntityDTO.class)
                    .block());
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnentity; }}Copy the code

Front end separation

Then there are some problems with the separation of the front and back ends. The access request is Ajax, but the Ajax request cannot be redirected by 302. You need to make an agreement with the front end that when the client needs to log in, it will return JSON and let the front end redirect itself. The backend implements an AuthenticationEntryPoint into the SpringSecurity setting. Much like Postman, the postman test single point does not work either. The redirection request will be forwarded directly in the background until 401, when you click on Postman, thinking it was only requested once. In fact, the Postman console shows that the request has been sent n times, without any hint

Spring OAuth2 redirects the savedRequest from the HttpSessionRequestCache after logging in. Spring OAuth2 redirects the savedRequest from the HttpSessionRequestCache after logging in. Spring OAuth2 redirects the savedRequest to the backend interface after logging in. Can customize AuthenticationSuccessHandler, tucked into loginSuccessHandler, jump to the fixed page, or in AuthenticationEntryPoint, The request will have a custom header, which is the JS code window.location.href, Then in the org. Springframework. Security. Web. AuthenticationEntryPoint# commence method execution time out from the header, replace the savedRequest in the Session, The replacement method is not shown, HttpSessionRequestCache is replaced however it is stored.

Then there is the Nginx reverse proxy, which needs to be set

proxy_set_header Host   $http_host;
Copy the code

Otherwise the org. Springframework. Security. Web. Authentication. LoginUrlAuthenticationEntryPoint# buildRedirectUrlToLoginPage request method .getServername () returns the Nginx host address instead of the original host address

By the way, if the IP address used in the development stage is tested and the domain name is different, if the domain name is used but the Nginx reverse proxy is not used, the three different port numbers of front-end page, background and single sign-on will lead to Cookies cannot be transmitted correctly, and session information will be lost after login. But the different port numbers of the digital IP can transmit Cookies correctly…

The development took a long time, the development finished, the deployment phase has been pit many times, took a week to calculate the deployment