The versatility of Spring Security, its high level of component abstraction, and the variety of configurations result in powerful and complex features. The cost of learning Spring Security is almost the highest in the Spring family. The sophisticated design of Spring Security is worth learning, but combined with the actual complex business scenarios, Not only do we need to understand how Spring Security extends, but we also need to understand how some of the components work and flow (how else can we inherit and rewrite what we need to rewrite?). Therefore, before deciding to use Spring Security to build a whole Security system (authorization, authentication, permissions, auditing), we still need to consider how complex our business will be in the future, and whether it is better to write a Security system by hand or to use Spring Security.

It is impossible to cover all aspects of Spring Security in a short article. In my recent work, I will be more exposed to OAuth2, so this article will briefly explain how to use Spring Security to build a set of OAuth2 authorization &SSO architecture.

OAuth2 profile

OAuth2.0 is an open standard set of authorization systems that defines four roles:

  1. The resource owner, the user, is used to grant permissions to third-party applications
  2. Clients, that is, third-party applications, require user authorization before they can access user resources
  3. To provide resources, resource providers, or resource servers, need to implement Token and ClientID verification, as well as proper permission control
  4. Authorization server, authenticates user identity, issues tokens to clients, and maintains and manages ClientID, tokens, and users

The last three can be separate programs, and in this example we will create separate projects for each of them. The OAuth2.0 standard defines four authorization modes at the same time. Here are the three most commonly used and the three that will be demonstrated later (Token =Token, Code =Code, may be mixed) :

  1. Regardless of the mode, the general process is as follows:
    • Third party websites (or clients) need to apply for a set of access ClientID+ClientSecret from the authorized server
    • Get the access Token in either mode (see process below)
    • Take the access Token to the resource server to request resources
    • The resource server controls the permission based on the queried Token
  2. Authorization code mode, the most standard and most secure mode, suitable for external interaction, the process is:
    • The third-party website client transfers to the authorization server, and sends ClientID, authorization Scope, redirection address RedirectUri and other information
    • The user logs in to the authorization server and approves the authorization. (The authorization step can be configured to be automatic.)
    • After authorization is complete, the redirection is returned to the redirection address provided by the client and the authorization code is attached
    • The third party website server uses authorization code +ClientID+ClientSecret to access the authorization server for Token (Token includes access Token and refresh Token. After the access Token passes, refresh Token is used to obtain a new access Token)
    • You may ask why this pattern is so complicated and why is it safe? Because we will not expose ClientSecret or access Token to the public, the process of exchanging the Token with the authorization code is carried out by the server, and the client only gets the one-time authorization code
  3. Password credential mode, suitable for use between internal systems (the client is an ally, the client needs to get the user account password), the process is as follows:
    • The user provides the account password to the client
    • The client uses the user’s account password and its own ClientID+ClientSecret to access the authorization server for Token exchange
  4. Client mode, suitable for use between internal servers:
    • It has nothing to do with the user and is not user-based authorization
    • Clients with their ClientID+ClientSecret to the authorization server for Token exchange

Now, let’s build a program to actually experience these patterns.

Setting up the Authorization Server

Start by creating a parent POM with three modules:

<? xml version="1.0" encoding="UTF-8"? > <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> < modelVersion > 4.0.0 < / modelVersion > < groupId > me. Josephzhu < / groupId > < artifactId > springsecurity101 < / artifactId > < packaging > pom < / packaging > < version > 1.0 - the SNAPSHOT < / version > < the parent > < groupId > org. Springframework. Boot < / groupId > <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> </parent> <modules>  <module>springsecurity101-cloud-oauth2-client</module> <module>springsecurity101-cloud-oauth2-server</module> <module>springsecurity101-cloud-oauth2-userservice</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> < project. Reporting. OutputEncoding > utf-8 < / project. Reporting. OutputEncoding > < Java version > 1.8 < / Java version > </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
Copy the code

Then we create the first module, resource server:

<? xml version="1.0" encoding="UTF-8"? > <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId> SpringSecurity101 </artifactId> <groupId>me. Josephzhu </groupId> <version>1.0-SNAPSHOT</version> < / parent > < modelVersion > 4.0.0 < / modelVersion > < artifactId > springsecurity101 - cloud - oauth2 - server < / artifactId > <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency>  <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>Copy the code

In addition to OAuth2 launcher of Spring Cloud, we also use data access, Web and other dependencies, because our resource server needs to use database to store client information, user information and other data, and we also use Thymeleaf to beautify the login page a little. Now let’s create a configuration file application.yml:

server: port: 8080 spring: application: name: oauth2-server datasource: url: jdbc:mysql://localhost:3306/oauth? useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
Copy the code

As you can see, we’re going to use the Oauth database, and the authorization server has port 8080. We need to initialize some tables in the database:

  1. User table Users: stores user names and passwords
  2. Authorities: Stores the user’s permissions
  3. Client information table oAUTH_client_details: stores the client ID, password, permission, ID of the resource server that can be accessed, and authorization mode that can be used
  4. Authorization code table oAUTH_code: stores authorization codes
  5. Approvals table OAUTH_Approvals: Shows approvals of user authorizations to third party servers

DDL is as follows:

-- ----------------------------
-- Table structure for authorities
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL.`authority` varchar(50) NOT NULL.UNIQUE KEY `ix_auth_username` (`username`.`authority`),
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL.`clientId` varchar(256) DEFAULT NULL.`partnerKey` varchar(32) DEFAULT NULL.`scope` varchar(256) DEFAULT NULL.`status` varchar(10) DEFAULT NULL.`expiresAt` datetime DEFAULT NULL.`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(255) NOT NULL.`resource_ids` varchar(255) DEFAULT NULL.`client_secret` varchar(255) DEFAULT NULL.`scope` varchar(255) DEFAULT NULL.`authorized_grant_types` varchar(255) DEFAULT NULL.`web_server_redirect_uri` varchar(255) DEFAULT NULL.`authorities` varchar(255) DEFAULT NULL.`access_token_validity` int(11) DEFAULT NULL.`refresh_token_validity` int(11) DEFAULT NULL.`additional_information` varchar(4096) DEFAULT NULL.`autoapprove` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`client_id`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50) NOT NULL.`password` varchar(100) NOT NULL.`enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(255) DEFAULT NULL.`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Copy the code

You’ll see the data in these tables later in the demo. You can see that we did not create a table in the database to hold the access token and refresh token, because our later implementation would transfer the token information using JWT and not in the database. Almost all of these tables are self-extensible, just by inheriting existing classes that implement Spring. Next, we create a core class to configure the authorization server:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.sql.DataSource;
import java.util.Arrays;

@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;

    /** * code 1 *@param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    /** * code 2 *@param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    /** * code 3 *@param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(
                Arrays.asList(tokenEnhancer(), jwtTokenEnhancer()));

        endpoints.approvalStore(approvalStore())
                .authorizationCodeServices(authorizationCodeServices())
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager);
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(a) {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    @Bean
    public TokenStore tokenStore(a) {
        return new JwtTokenStore(jwtTokenEnhancer());
    }

    @Bean
    public JdbcApprovalStore approvalStore(a) {
        return new JdbcApprovalStore(dataSource);
    }

    @Bean
    public TokenEnhancer tokenEnhancer(a) {
        return new CustomTokenEnhancer();
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer(a) {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
        return converter;
    }

    /** * code 4 */
    @Configuration
    static class MvcConfig implements WebMvcConfigurer {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("login").setViewName("login"); }}}Copy the code

Analyze this class:

  1. First of all, we can see, we need to open the authorization by annotation @ EnableAuthorizationServer server
  2. In code snippet 1, we configure the database to maintain client information. Of course, in various demos, we often see the client information in memory, and write it directly in the configuration. For real applications, we usually use the database to maintain this information. A workflow will even be set up to allow clients to apply for ClientID themselves
  3. In snippet 2, we do two things for the security of the authorization server, first turning on authentication Token access (for later demonstration), and then allowing ClientSecret to be saved in plaintext and submitted through forms (not just Basic Auth), which we’ll show later
  4. In snippet 3, we do several things:
    • Configure our Token storage mode not in memory mode, not in database mode, not in Redis mode, but in JWT mode. JWT is short for Json Web Token, which is a Token packaged in Json data format. The period separates the whole JWT into three parts: header, data body and signature. JWT stores tokens, although easy to use, but not so secure. They are generally used internally, and require HTTPS+ to configure a relatively short expiration time
    • Asymmetric encryption of the JWT Token is configured for signature
    • A custom Token enhancer was configured to put more information into tokens
    • The JDBC database is configured to save user authorization approval records
  5. In code snippet 4, we configure the view information for the login page (a separate configuration class could be more formal)

We need to add a few things to the resources directory. First we need to create a templates folder in the resources directory and then create a login.html login template:

<! DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" class="uk-height-1-1">
<head>
    <meta charset="UTF-8"/>
    <title>OAuth2 Demo</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.26.3/css/uikit.gradient.min.css"/>
</head>

<body class="uk-height-1-1">

<div class="uk-vertical-align uk-text-center uk-height-1-1">
    <div class="uk-vertical-align-middle" style="width: 250px;">
        <h1>Login Form</h1>

        <p class="uk-text-danger" th:if="${param.error}"> Wrong username or password... </p> <form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/login}">
            <div class="uk-form-row">
                <input class="uk-width-1-1 uk-form-large" type="text" placeholder="Username" name="username"
                       value="reader"/>
            </div>
            <div class="uk-form-row">
                <input class="uk-width-1-1 uk-form-large" type="password" placeholder="Password" name="password"
                       value="reader"/>
            </div>
            <div class="uk-form-row">
                <button class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Login</button>
            </div>
        </form>

    </div>
</div>
</body>
</html>
Copy the code

Then, we need to use the keytool tool to generate the key, save the key file JKS to a directory, and export a public key for future use. We also used a custom Token enhancer in the code, which is implemented as follows:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

public class CustomTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Authentication userAuthentication = authentication.getUserAuthentication();
        if(userAuthentication ! =null) {
            Object principal = authentication.getUserAuthentication().getPrincipal();
            Map<String, Object> additionalInfo = new HashMap<>();
            additionalInfo.put("userDetails", principal);
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        }
        returnaccessToken; }}Copy the code

This code is very simple. It simply stores the user information into the Token with the userDetails Key (this code will not work if the authorization mode is client mode, because it does not matter to the user). This is a common requirement. By default, only basic information such as the user name is used in the Token, and we often need to return more information about the user to the client (in practice, you may need to query more user information from the database or external services to add to the JWT Token). At this point you can customize the enhancer to enrich the Token’s content. Now that the core configuration for the authorization server is complete, let’s implement the security configuration:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.sql.DataSource;


@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login"."/oauth/authorize")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login"); }}Copy the code

Here we did two main things:

  1. Configure the authentication mode for the user account. Obviously, we have stored the user in the database in the way we want to configure JDBC. In addition, we have configured the BCryptPasswordEncoder encryption to save the user password (the user password in production environment must not be saved in plain text).
  2. Open the anonymous access of /login and /oauth/authorize paths. The former is used for login and the latter is used for exchanging authorization codes. Both endpoints are accessed before login

Finally, configure a main program:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OAuth2ServerApplication {
    public static void main(String[] args) { SpringApplication.run(OAuth2ServerApplication.class, args); }}Copy the code

The authorization server configuration is complete.

Setting up the Resource Server

Create the project first:

<? xml version="1.0" encoding="UTF-8"? > <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId> SpringSecurity101 </artifactId> <groupId>me. Josephzhu </groupId> <version>1.0-SNAPSHOT</version> < / parent > < modelVersion > 4.0.0 < / modelVersion > < artifactId > springsecurity101 - cloud - oauth2 - userservice < / artifactId > <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies> </project>Copy the code

The configuration is extremely simple, declaring resource service port 8081

server:
  port: 8081
Copy the code

Remember the public key file we exported through the key before we dropped the resources folder, something like this:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD
mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z
w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe
h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l
3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk
LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul
+QIDAQAB
-----END PUBLIC KEY-----
Copy the code

Let’s create an anonymous interface called GET /hello:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("hello")
    public String hello(a) {
        return "Hello"; }}Copy the code

Let’s create an interface that requires login + authorization to access:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private TokenStore tokenStore;

    @PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')")
    @GetMapping("name")
    public String name(OAuth2Authentication authentication) {
        return authentication.getName();
    }

    @PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')")
    @GetMapping
    public OAuth2Authentication read(OAuth2Authentication authentication) {
        return authentication;
    }

    @PreAuthorize("hasAuthority('WRITE')")
    @PostMapping
    public Object write(OAuth2Authentication authentication) {
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(details.getTokenValue());
        return accessToken.getAdditionalInformation().getOrDefault("userDetails".null); }}Copy the code

Here we configure three interfaces and use @preauthorize to control permissions before methods execute:

  1. You can access the read and write permissions of the GET/User /name interfaces
  2. The GET /user interface can be accessed with read and write permissions, and the entire OAuth2Authentication is returned
  3. The POST/User interface, which is only accessible with write permission, returns the additional information that the previous CustomTokenEnhancer added to the Token. The Key is the userDetails, which also demonstrates how TokenStore is used to resolve the Token

Let’s create the core resource server configuration class:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.FileCopyUtils;

import java.io.IOException;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    /** * code 1 *@param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("foo").tokenStore(tokenStore());
    }
    
    @Bean
    public TokenStore tokenStore(a) {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    
    @Bean
    protected JwtAccessTokenConverter jwtAccessTokenConverter(a) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");
        String publicKey = null;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        converter.setVerifierKey(publicKey);
        return converter;
    }

    /** * code 2 *@param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated() .anyRequest().permitAll(); }}Copy the code

Here we did four things:

  1. @enableresourceserver Enable the resource server
  2. @ EnableGlobalMethodSecurity (prePostEnabled = true) annotations to enable method for access control
  3. Code 1 declares that the ID of the resource server is foo, that the TokenStore of the resource server is JWT, and that the public key
  4. In Code 2, requests other than the /user path are configured to be accessed anonymously

If the authorization server generates tokens, the resource server must have a way to verify the tokens. If it is not JWT, we can do this:

  1. Tokens can be stored in a database or Redis, and the resource server and authorization server share the underlying TokenStore for verification
  2. Resource servers can use RemoteTokenServices to perform Token verification from the authorization server’s /oauth/check_token endpoint (remember we opened this port earlier).

Now we are using the non-ground JWT + asymmetric encryption, which needs to be authenticated by the local public key, so here we configure the public key path. Finally create a startup class:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); }}Copy the code

At this point, the resource server configuration is complete, we also built two controllers in the resource server, used to test anonymous access and receive resource server permission protection.

Initialize data configuration

Now let’s look at how to configure the database implementation:

  1. Two users: Reader has the read permission, and writer has the read permission
  2. Two permissions, read and write
  3. Three clients:
    • Userservice1 This client uses the password credential mode
    • Userservice2 This client uses client mode
    • Userservice3 The client uses the authorization code mode

The first is the oAUTH_client_details table:

INSERT INTO `oauth_client_details` VALUES ('userservice1'.'foo'.'1234'.'FOO'.'password,refresh_token'.' '.'READ,WRITE', 7200, NULL, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('userservice2'.'foo'.'1234'.'FOO'.'client_credentials,refresh_token'.' '.'READ,WRITE', 7200, NULL, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('userservice3'.'foo'.'1234'.'FOO'.'authorization_code,refresh_token'.'https://baidu.com'.'READ,WRITE', 7200, NULL, NULL, 'false');
Copy the code

As mentioned earlier, there are three records configured:

  1. The resource ID they can use is foo, which corresponds to the configuration of our resource server UserService
  2. They are both granted scope FOO and can be granted read/write permissions (but in the case of user association patterns, the final permissions will depend on the intersection of client and user permissions)
  3. The grant_types field configures the different authorization modes that are supported. Here we have configured each of the three clients with one mode for test observation. You can configure the four modes that support OAuth2.0 for one client
  4. For userService1 and 2, we have configured automatic authorization for users.

/ / add user / / add user / / add user / / add user / / add user / / add user / / add user / / add user / / add user / / add user / / add user / /

INSERT INTO `authorities` VALUES ('reader'.'READ');
INSERT INTO `authorities` VALUES ('writer'.'READ,WRITE');
Copy the code

The users table configates the account names and passwords of two users:

INSERT INTO `users` VALUES ('reader'.'$2a$04$C6pPJvC1v6.enW6ZZxX.luTdpSI/1gcgTVN7LhvQV6l/AfmzNU/3i', 1);
INSERT INTO `users` VALUES ('writer'.'$2a$04$M9t2oVs3/VIreBMocOujqOaB/oziWL0SnlWdt8hV4YnlhQrORA0fS', 1);
Copy the code

Remember, we’re using BCryptPasswordEncoder for our passwords, and you can use some online tools to do that

Demonstrate three authorization modes

Client mode

POST request address: http://localhost:8080/oauth/token? Grant_type =client_credentials&client_id= userService2 &client_secret=1234

http://localhost:8080/oauth/check_token?client_id=userservice1&client_secret=1234&token=

Password credential mode

POST request address: http://localhost:8080/oauth/token? Grant_type =password&client_id= userService1&client_secret =1234&username=writer&password=writer The following result is obtained:

Authorization code mode

First open the browser to access the address: http://localhost:8080/oauth/authorize? Response_type =code&client_id= userService3 &redirect_uri=https://baidu.com After login, the page will jump to the login interface, using reader:

www.baidu.com/?code=O8RiC…

http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=userservice3&client_secret=1234&code=O8RiCe&re direct_uri=https://baidu.com

Demonstrates resource server permission control

First we can test our security configuration to access /hello endpoints anonymously without authentication:

http://localhost:8081/user/

Build the client program

Before, we used a bare HTTP request to apply for and use tokens manually. Finally, we set up an OAuth client program to automatically implement this process:

<? xml version="1.0" encoding="UTF-8"? > <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId> SpringSecurity101 </artifactId> <groupId>me. Josephzhu </groupId> <version>1.0-SNAPSHOT</version> < / parent > < artifactId > springsecurity101 - cloud - oauth2 - client < / artifactId > < modelVersion > 4.0.0 < / modelVersion > <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>Copy the code

The configuration file is as follows:

server:
  port: 8082
  servlet:
    context-path: /ui
security:
  oauth2:
    client:
      clientId: userservice3
      clientSecret: 1234
      accessTokenUri: http://localhost:8080/oauth/token
      userAuthorizationUri: http://localhost:8080/oauth/authorize
      scope: FOO
    resource:
      jwt:
        key-value: |
          -----BEGIN PUBLIC KEY-----
          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD
          mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z
          w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe
          h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l
          3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk
          LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul
          +QIDAQAB
          -----END PUBLIC KEY-----
spring:
  thymeleaf:
    cache: false

#logging:
# level:
# ROOT: DEBUG
Copy the code

Client project port 8082, a few caveats:

  1. In the local test, we need to configure context-path; otherwise, CSRF defense may be triggered due to Cookie interference between the client and authorization server. After this problem occurs, no error log is generated in the program. Only after the DEBUG mode is enabled, you can see a message in the DEBUG log. This problem is very difficult to troubleshoot, and I don’t know why Spring doesn’t have this information as a WARN level
  2. As the OAuth client, we need to configure the address for the OAuth server to obtain the token and the address for authorization (obtaining the authorization code), as well as the ID and password of the client, and the authorization scope
  3. Since we are using the JWT Token, we need to configure the public key (of course, we can also configure the public key from the authorization server side if we do not configure the public key directly here).

First implement the MVC configuration:

package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public RequestContextListener requestContextListener(a) {
        return new RequestContextListener();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/")
                .setViewName("forward:/index");
        registry.addViewController("/index"); }}Copy the code

Two things are done here:

  1. Configure the RequestContextListener Bean to enable the Session Scope
  2. Configure the index path’s home page Controller and then implement the security configuration:
package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@Order(200)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/"."/login**") .permitAll() .anyRequest() .authenticated(); }}Copy the code

Here we implement the/path and /login path to allow access, other paths require authentication to access. Then we’ll create a controller:

package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class DemoController {
    @Autowired
    OAuth2RestTemplate restTemplate;

    @GetMapping("/securedPage")
    public ModelAndView securedPage(OAuth2Authentication authentication) {
        return new ModelAndView("securedPage").addObject("authentication", authentication);
    }

    @GetMapping("/remoteCall")
    public String remoteCall(a) {
        ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://localhost:8081/user/name", String.class);
        returnresponseEntity.getBody(); }}Copy the code

You can see it here:

  1. For securedPage, we pass user information into the view as a model
  2. We introduced the OAuth2RestTemplate, which allows you to fetch resources directly from the resource server using credentials after login, without the cumbersome implementation of obtaining access tokens. The process of adding access tokens to the request header started with the index page defined as follows:
<! DOCTYPE html> <html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Spring Security SSO Client</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/>
</head>

<body>
<div class="container">
    <div class="col-sm-12">
        <h1>Spring Security SSO Client</h1>
        <a class="btn btn-primary" href="securedPage">Login</a>
    </div>
</div>
</body>
</html>
Copy the code

The securedPage page is now defined as follows:

<! DOCTYPE html> <html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Spring Security SSO Client</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/>
</head>

<body>
<div class="container">
    <div class="col-sm-12">
        <h1>Secured Page</h1>
        Welcome, <span th:text="${authentication.name}">Name</span>
        <br/>
        Your authorities are <span th:text="${authentication.authorities}">authorities</span>
    </div>
</div>
</body>
</html>
Copy the code

The next critical step is to enable @enableoAuth2SSO. This annotation contains @enableoAuth2Client:

package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

@Configuration
@EnableOAuth2Sso
public class OAuthClientConfig {
    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails details) {
        return newOAuth2RestTemplate(details, oAuth2ClientContext); }}Copy the code

In addition, we also define the OAuth2RestTemplate here, some of the older data given online is manual reading configuration files, the latest version has injected OAuth2ProtectedResourceDetails automatically. Finally, the startup class:

package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OAuth2ClientApplication {
    public static void main(String[] args) { SpringApplication.run(OAuth2ClientApplication.class, args); }}Copy the code

Demonstrate single sign-on

Start the client project, open the browser to http://localhost:8082/ui/securedPage: can see page automatically transferred to the login page in license server:

baidu.com
http://localhost:8082/ui/login,http://localhost:8083/ui/login,http://localhost:8082/ui/remoteCall

Demo client requests resource server resources

Finally, let’s access the remoteCall interface:

@PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')")
@GetMapping("name")
public String name(OAuth2Authentication authentication) {
    return authentication.getName();
}
Copy the code

Try using another user:

conclusion

This article uses OAuth 2.0 as a dimension to get a glimpse of the functions of Spring Security, introduces the basic concepts of OAuth 2.0, experience the three common modes, and also uses Spring Security to implement the three components of OAuth 2.0. Client, authorization server and resource server, implement the resource server permission control, and finally use the client to test SSO and OAuth2RestTemplate use, all the code see my Github github.com/JosephZhu19… Hope you found this article useful.