Information Security 17 min read

How to Build an OAuth2 Authorization Server with Spring Boot, Redis, and Custom Token Store

This guide walks through creating a Spring Boot 2.2.11 OAuth2 authorization server that stores tokens in Redis, defines custom client and user entities, configures grant types, and demonstrates testing each flow, complete with code snippets and configuration files.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build an OAuth2 Authorization Server with Spring Boot, Redis, and Custom Token Store

In this tutorial we set up an OAuth2 authorization server using Spring Boot 2.2.11, Redis for token storage, and custom JPA entities for applications and users.

Dependencies

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
  &lt;artifactId&gt;commons-pool2&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.security.oauth.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-security-oauth2-autoconfigure&lt;/artifactId&gt;
  &lt;version&gt;2.2.11.RELEASE&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;mysql&lt;/groupId&gt;
  &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;net.sourceforge.nekohtml&lt;/groupId&gt;
  &lt;artifactId&gt;nekohtml&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;
&lt;/dependency&gt;</code>

Application Configuration (application.yml)

<code>server:
  port: 8208
---
spring:
  application:
    name: oauth-server
---
spring:
  redis:
    host: localhost
    port: 6379
    password:
    database: 1
    lettuce:
      pool:
        maxActive: 8
        maxIdle: 100
        minIdle: 10
        maxWait: -1
---
spring:
  resources:
    staticLocations: classpath:/static/,classpath:/templates/,classpath:/pages/
  mvc:
    staticPathPattern: /resources/**
---
spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimumIdle: 10
      maximumPoolSize: 200
      autoCommit: true
      idleTimeout: 30000
      poolName: MasterDatabookHikariCP
      maxLifetime: 1800000
      connectionTimeout: 30000
      connectionTestQuery: SELECT 1
  jpa:
    hibernate:
      ddlAuto: update
    showSql: true
    openInView: true #Open EntityManager in View
---
spring:
  thymeleaf:
    servlet:
      contentType: text/html; charset=utf-8 
    cache: false
    mode: LEGACYHTML5
    encoding: UTF-8
    enabled: true
    prefix: classpath:/pages/
    suffix: .html
---
spring:
  main:
    allow-bean-definition-overriding: true</code>

Entity Classes

<code>@Entity
@Table(name = "T_APP")
public class App implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    /** 客户端ID */
    private String clientId;
    /** 客户端密钥 */
    private String clientSecret;
    /** 跳转地址 */
    private String redirectUri;
}
// This entity stores each application's information.

@Entity
@Table(name = "T_USERS")
public class Users implements UserDetails, Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    private String username;
    private String password;
    // This entity stores user login information.
}</code>

Repository Interfaces

<code>public interface AppRepository extends JpaRepository<App, String>, JpaSpecificationExecutor<App> {
    App findByClientId(String clientId);
}

public interface UsersRepository extends JpaRepository<Users, String>, JpaSpecificationExecutor<Users> {
    Users findByUsernameAndPassword(String username, String password);
}</code>

Core Configuration Class

<code>@Configuration
@EnableAuthorizationServer
public class OAuthAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    private AppRepository appRepository;
    @Resource
    private RedisConnectionFactory redisConnectionFactory;
    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // Custom authorization code service
        endpoints.authorizationCodeServices(new InMemoryAuthorizationCodeServices() {
            @Override
            public String createAuthorizationCode(OAuth2Authentication authentication) {
                String code = UUID.randomUUID().toString().replaceAll("-", "");
                store(code, authentication);
                return code;
            }
        });
        // Custom exception translation
        endpoints.exceptionTranslator(new DefaultWebResponseExceptionTranslator() {
            @SuppressWarnings({"unchecked", "rawtypes"})
            @Override
            public ResponseEntity translate(Exception e) throws Exception {
                ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
                ResponseEntity<Map<String, Object>> customEntity = exceptionProcess(responseEntity);
                return customEntity;
            }
        });
        // Enable password grant type
        endpoints.authenticationManager(authenticationManager);
        // Token services (JWT, validity, etc.)
        endpoints.tokenServices(tokenService());
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.values());
        endpoints.accessTokenConverter(defaultTokenConvert());
        endpoints.tokenStore(tokenStore());
        endpoints.pathMapping("/oauth/error", "/oauth/customerror");
        endpoints.requestValidator(new OAuth2RequestValidator() {
            @Override
            public void validateScope(AuthorizationRequest authorizationRequest, ClientDetails client) throws InvalidScopeException {}
            @Override
            public void validateScope(TokenRequest tokenRequest, ClientDetails client) throws InvalidScopeException {}
        });
        endpoints.approvalStore(new InMemoryApprovalStore());
    }

    @Bean
    public ClientDetailsService clientDetailsService() {
        return clientId -> {
            if (clientId == null) {
                throw new ClientRegistrationException("未知的客户端: " + clientId);
            }
            App app = appRepository.findByClientId(clientId);
            if (app == null) {
                throw new ClientRegistrationException("未知的客户端: " + clientId);
            }
            OAuthClientDetails clientDetails = new OAuthClientDetails();
            clientDetails.setClientId(clientId);
            clientDetails.setClientSecret(app.getClientSecret());
            Set<String> redirectUris = new HashSet<>();
            redirectUris.add(app.getRedirectUri());
            clientDetails.setRegisteredRedirectUri(redirectUris);
            clientDetails.setScoped(false);
            clientDetails.setSecretRequired(true);
            clientDetails.setScope(new HashSet<>());
            Set<String> grantTypes = new HashSet<>();
            grantTypes.add("authorization_code");
            grantTypes.add("implicit");
            grantTypes.add("password");
            grantTypes.add("refresh_token");
            grantTypes.add("client_credentials");
            clientDetails.setAuthorizedGrantTypes(grantTypes);
            clientDetails.setAuthorities(new ArrayList<>());
            return clientDetails;
        };
    }

    @Bean
    public TokenEnhancer tokenEnhancer(){
        return (accessToken, authentication) -> {
            System.out.println(authentication);
            if (accessToken instanceof DefaultOAuth2AccessToken) {
                DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
                Map<String, Object> additionalInfo = new LinkedHashMap<>();
                additionalInfo.put("username", ((Users)authentication.getPrincipal()).getUsername());
                additionalInfo.put("create_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                token.setAdditionalInformation(additionalInfo);
            }
            return accessToken;
        };
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices tokenService = new DefaultTokenServices();
        tokenService.setSupportRefreshToken(true);
        tokenService.setReuseRefreshToken(true);
        tokenService.setTokenEnhancer(tokenEnhancer());
        tokenService.setTokenStore(tokenStore());
        tokenService.setAccessTokenValiditySeconds(60 * 60 * 24 * 3);
        tokenService.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
        return tokenService;
    }

    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public DefaultAccessTokenConverter defaultTokenConvert() {
        return new DefaultAccessTokenConverter();
    }

    private static ResponseEntity<Map<String, Object>> exceptionProcess(ResponseEntity<OAuth2Exception> responseEntity) {
        Map<String, Object> body = new HashMap<>();
        body.put("code", -1);
        OAuth2Exception ex = responseEntity.getBody();
        String msg = ex.getMessage();
        if (msg != null) {
            body.put("message", "认证失败,非法用户");
        } else {
            String err = ex.getOAuth2ErrorCode();
            body.put("message", err != null ? err : "认证服务异常,未知错误");
        }
        body.put("data", null);
        return new ResponseEntity<>(body, responseEntity.getHeaders(), responseEntity.getStatusCode());
    }
}
</code>

Expose AuthenticationManager for Password Grant

<code>@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
</code>

Custom ClientDetails Implementation

<code>public class OAuthClientDetails implements ClientDetails, Serializable {
    private static final long serialVersionUID = 1L;
    private String id;
    private String clientId;
    private boolean secretRequired;
    private String clientSecret;
    private boolean scoped;
    private Set<String> resourceIds;
    private Set<String> scope = new HashSet<>();
    private Set<String> authorizedGrantTypes = new HashSet<>();
    private Set<String> registeredRedirectUri = new HashSet<>();
    private Collection<GrantedAuthority> authorities;
    private boolean autoApprove;
    private Integer accessTokenValiditySeconds;
    private Integer refreshTokenValiditySeconds;
    // getters and setters omitted for brevity
}
</code>

Login Authentication Provider

<code>@Component
public class LoginAuthenticationProvider implements AuthenticationProvider {
    @Resource
    private UsersRepository usersRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        Object credentials = authentication.getCredentials();
        Users user = usersRepository.findByUsernameAndPassword(username, (String) credentials);
        if (user == null) {
            throw new BadCredentialsException("错误的用户名或密码");
        }
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                user, authentication.getCredentials(),
                Arrays.asList(new SimpleGrantedAuthority("ROLE_USERS"), new SimpleGrantedAuthority("ROLE_ACTUATOR")));
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
</code>

Password Encoder (No‑Op)

<code>@Component
public class LoginPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encode(rawPassword).equals(encodedPassword);
    }
}
</code>

OAuth2 Grant Types Overview

Authorization Code Grant – the most complete flow; the client’s backend exchanges an authorization code for a token.

Authorization Code Flow
Authorization Code Flow

Password Grant – the resource owner provides username and password directly to the client; used only when the client is highly trusted.

Password Grant Flow
Password Grant Flow

Client Credentials Grant – the client authenticates as itself without user involvement.

Client Credentials Flow
Client Credentials Flow

Implicit Grant – token is returned directly in the browser’s URL fragment, skipping the authorization code step.

Implicit Grant Flow
Implicit Grant Flow

Refresh Token – allows a client to obtain a new access token before the current one expires.

Refresh Token Flow
Refresh Token Flow

Testing the Server

Create two records in the

T_APP

and

T_USERS

tables (screenshots omitted for brevity). Then test each grant type:

Authorization Code – navigate to the authorization endpoint, log in, obtain the one‑time code, and exchange it for a token.

Password Grant – send a POST request with

grant_type=password

, username, and password to obtain a token.

Client Credentials – request a token using only client ID and secret.

Implicit Grant – open the authorization URL in a browser and retrieve the token from the URL fragment.

Refresh Token – use the returned

refresh_token

to get a new access token.

All flows work as expected, confirming that the OAuth2 server is correctly integrated with Redis token storage and custom client details.

Finally, remember to follow the server’s instructions to like, share, or follow the author for updates.

QR Code
QR Code
JavaRedisSpring BootsecurityOAuth2authorization-serverToken Store
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.