Design and Implementation of Authentication and API Permission Control in Microservice Architecture: Authorization Code Mode

Introduction to Authorization Code Types

The authorization code type (authorization code) allows the resource owner to directly interact with the authorization server for authorization through redirection, which avoids the leakage of resource owner information to the client. It is the authorization type with the most complete functions and the most rigorous process, but It is required that the client must be able to interact with the resource owner's proxy (usually a web browser) and accept requests from the authorization server (redirect to give the authorization code). The authorization process is as follows:

+----------+
| Resource |
|   Owner  |
|          |
+----------+
 ^
 |
(B)
+----|-----+ Customer Identifier +---------------+
|         -+----(A)-- & Redirection URI ---->|               |
|  User-   |                                 | Authorization |
|  Agent  -+----(B)-- User authenticates --->|     Server    |
|          |                                 |               |
|         -+----(C)-- Authorization Code ---<|               |
+-|----|---+                                 +---------------+
| | ^v
(A)  (C)                                        |      |
|    |                                         |      |
^ v | |
+---------+                                      |      |
|         |>---(D)-- Authorization Code ---------'      |
|  Client |          & Redirection URI                  |
|         |                                             |
|         |<---(E)----- Access Token -------------------'
+---------+       (w/ Optional Refresh Token)

 

  1. The client directs the resource owner's user agent to the authorization server's endpoint, typically by redirection. The information submitted by the client should include the client identifier, the requested scope, the local state, and the redirection URI for returning the authorization code.
  2. The authorization server authenticates the resource owner (via the user agent) and confirms whether the resource owner allows or denies the client's access request
  3. If the resource owner grants the client access, the authorization server calls back to the redirect address provided by the client by redirecting the user agent, adding the authorization code and any local state previously provided by the client to the redirect address
  4. The client carries the authorization code obtained in the previous step to request an access token from the authorization server. In this step both the authorization code and the client are authenticated by the authorization server. The client needs to submit the redirection address used to obtain the authorization code
  5. The authorization server authenticates the client and authenticates the authorization code, ensuring that the redirection address received matches the redirection address used to obtain the authorization code in the third step. If valid, return the access token, possibly a Refresh Token

 

Quick start

Spring-Securiy configuration

Since the authorization code mode requires the login user to authorize the client requesting access_token, the auth-server needs to add Spring-Security related configuration to guide the user to log in.

On the original basis, configure Spring-Securiy to allow users to perform form login:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomLogoutHandler customLogoutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
            .requestMatchers().antMatchers("/**")
            .and().authorizeRequests()
            .antMatchers("/**").permitAll()
            .anyRequest().authenticated()
            .and().formLogin()
            .permitAll()
            .and().logout()
            .logoutUrl("/logout")
            .clearAuthentication(true)
            .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
            .addLogoutHandler (customLogoutHandler);
}


At the same time, it is necessary to migrate the logout port processing in the resource server in ResourceServerConfig to WebSecurityConfig, and comment out the HttpSecurity configuration of ResourceServerConfig:

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//    @Override
//    public void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable()
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .and()
//                .requestMatchers().antMatchers("/**")
//                .and().authorizeRequests()
//                .antMatchers("/**").permitAll()
//                .anyRequest().authenticated()
//                .and().logout()
//                .logoutUrl("/logout")
//                .clearAuthentication(true)
//                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
// .addLogoutHandler (customLogoutHandler ());
//
//        //http.antMatcher("/api/**").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);
//
//    }
/*   @Bean
public CustomSecurityFilter customSecurityFilter() {
    return new CustomSecurityFilter();
}
*/
.....

 

AuthenticationProvider

Since the authentication process of the user form login may be different, add a CustomSecurityAuthenticationProvider for this, which is basically the same as the CustomAuthenticationProvider, but ignores the authentication and processing of the client client.

@Component
public class CustomSecurityAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserClient userClient;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String username = authentication.getName();
    String password;
    Map map;
    password = (String) authentication.getCredentials();
    //If you are calling the user service, you don't need to note it here
    //map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
    map = checkUsernameAndPassword(getUserServicePostObject(username, password));
    String userId = (String) map.get("userId");
    if (StringUtils.isBlank(userId)) {
        String errorCode = (String) map.get("code");
        throw new BadCredentialsException(errorCode);
    }
    CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId);
    return new CustomAuthenticationToken(customUserDetails);
}
private CustomUserDetails buildCustomUserDetails(String username, String password, String userId) {
    CustomUserDetails customUserDetails = new CustomUserDetails.CustomUserDetailsBuilder()
            .withUserId(userId)
            .withPassword(password)
            .withUsername(username)
            .withClientId("for Security")
            .build();
    return customUserDetails;
}
private Map<String, String> getUserServicePostObject(String username, String password) {
    Map<String, String> requestParam = new HashMap<String, String>();
    requestParam.put("userName", username);
    requestParam.put("password", password);
    return requestParam;
}
/ / Simulate the method of calling the user service
private Map checkUsernameAndPassword(Map map) {
    //checkUsernameAndPassword
    Map ret = new HashMap();
    ret.put("userId", UUID.randomUUID().toString());
    return ret;
}
@Override
public boolean supports(Class<?> aClass) {
    return true;
}


Add the CustomSecurityAuthenticationProvider configuration in AuthenticationManagerConfig:

@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
CustomSecurityAuthenticationProvider securityAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(customAuthenticationProvider)
            .authenticationProvider(securityAuthenticationProvider);
}


Ensure that the requesting client in the database has the request authorization of the authorization code and has the callback address, and the callback address is used to accept the authorization code.

authority1.png

 

test use

Start the service, and the browser accesses the address http://localhost:9091/oauth/au ... mp%3B scope=all&redirect_uri= http://localhost:8080 .

Redirect to the login interface and guide the user to log in:

authority2.png


The login is successful and the client is authorized to obtain the authorization code.

authority3.png


After authorization, get the authorization code from the callback address:

http://localhost:8080/?code=7OglOJ


Carry the authorization code to obtain the corresponding token:

authority4.png

 

authority5.png

 

Detailed source code

AuthorizationServerTokenServices is an interface for token operations in the authorization server, providing the following three interfaces:

public interface AuthorizationServerTokenServices {
// Generate access_token bound to OAuth2 authentication
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
// Refresh access_token according to refresh_token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
        throws AuthenticationException;
// Get access_token for OAuth2 authentication, if access_token exists
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);


Note that the generated tokens are bound to authorized users.

The default implementation of the AuthorizationServerTokenServices interface is DefaultTokenServices. Note that tokens are saved and managed through TokenStore.

Generate token:

//DefaultTokenServices
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// Get access_token from TokenStore
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
    if (existingAccessToken.isExpired()) {
        // if access_token already exists but expired
        // delete the corresponding access_token and refresh_token
        if (existingAccessToken.getRefreshToken ()! = null) {
            refreshToken = existingAccessToken.getRefreshToken ();
            tokenStore.removeRefreshToken(refreshToken);
        }
        tokenStore.removeAccessToken(existingAccessToken);
    }
    else {
        // if the access_token already exists and has not expired
        // Re-save to prevent authentication changes, and return the access_token
        tokenStore.storeAccessToken(existingAccessToken, authentication);
        return existingAccessToken;
    }
}
// Recreate a new refresh_token only if refresh_token is null
// This will enable clients holding expired access_token to get recreated access_token based on previous refresh_token
// Because the created access_token needs to bind refresh_token
if (refreshToken == null) {
    refreshToken = createRefreshToken(authentication);
}else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
    // If refresh_token also has an expiration date and expires, recreate it
    ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
    if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
        refreshToken = createRefreshToken(authentication);
    }
}
// Bind authorized user and refresh_token to create new access_token
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
// Save the access_token corresponding to the authorized user
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken ();
if (refreshToken != null) {
    // Save the refresh_token corresponding to the authorized user
    tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;


It should be noted that in the process of creating the token, the authorized user will check whether there is an unexpired access_token, and if there is one, it will be returned directly. Then create an access_token. This is to prevent the expired access_token from being able to regain the access_token through refresh_token, because the same refresh_token is bound to the same refresh_token created before and after the access_token.

I recommend an exchange and learning group to you: 575745314, which will share some videos recorded by senior architects: Spring, MyBatis, Netty source code analysis, principles of high concurrency, high performance, distributed, microservice architecture, JVM performance optimization, etc. The body of knowledge necessary to become an architect. You can also receive free learning resources, which have benefited a lot at present. The following course system diagram is also obtained in the group.

The refreshAccessToken() method for refreshing the token and the getAccessToken() method for obtaining the token in DefaultTokenServices are left to the readers to check and will not be introduced here.

summary

This article mainly talks about the authorization code mode. In the authorization code mode, the user needs to authorize after logging in to obtain the authorization code, and then carry the authorization code to TokenEndpoint to request an access token. Of course, you can also set response_token=token in the request directly through the implicit type. Get access_token. There is a problem to note here. When reaching the AuthorizationEndpoint endpoint, the client is not authenticated, but the request must be authenticated by the user to be accepted.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324414785&siteId=291194637