Backend Development 16 min read

How to Build a Spring OAuth2 Authorization Server with Redis Token Store

This tutorial walks through setting up a Spring Boot 2.2.11 OAuth2 authorization server that stores tokens in Redis, covering Maven dependencies, YAML configuration, JPA entities, DAO interfaces, core server configuration, custom client details, authentication provider, password encoder, and detailed testing of all OAuth2 grant types.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build a Spring OAuth2 Authorization Server with Redis Token Store

Overview

The example uses Spring Boot 2.2.11.RELEASE with OAuth2 and Redis to store access tokens. Redis is employed as the token store, and the project includes JPA for persistence.

Dependencies (pom.xml)

<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
---
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 definitions

<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;
    private String clientId; // 客户端ID
    private String clientSecret; // 客户端密钥
    private String redirectUri; // 跳转地址
}

@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;
}</code>

DAO 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>

Authorization server configuration

<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 code and token services configuration omitted for brevity
        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenServices(tokenService());
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.values());
        endpoints.accessTokenConverter(defaultTokenConvert());
        endpoints.tokenStore(tokenStore());
        endpoints.pathMapping("/oauth/error", "/oauth/customerror");
    }

    @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) -> {
            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;
        };
    }

    @Primary
    @Bean
    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 != null ? ex.getMessage() : null;
        if (msg != null) {
            body.put("message", "认证失败,非法用户");
        } else {
            String err = ex != null ? ex.getOAuth2ErrorCode() : null;
            body.put("message", err != null ? err : "认证服务异常,未知错误");
        }
        body.put("data", null);
        return new ResponseEntity<>(body, responseEntity.getHeaders(), responseEntity.getStatusCode());
    }
}</code>

Authentication manager exposure

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

Custom ClientDetails class

<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

<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

The tutorial explains the five standard OAuth2 grant types supported by the server:

Authorization Code – the most secure flow, involving a backend exchange of a temporary code for an access token.

Password – the resource‑owner password credentials grant, where the client directly receives the user’s username and password.

Client Credentials – the client authenticates itself without a user context.

Implicit – suitable for browser‑based apps; the token is returned directly in the URL fragment.

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

Testing the implementation

Two sample records are inserted into the

T_APP

and

T_USERS

tables. The tutorial then shows how to request an authorization code, exchange it for a token, and use the password, client‑credentials, and implicit flows. Screenshots (included as images) illustrate each step, and the final note reminds that authorization codes are single‑use.

After successful token acquisition, the response contains both

access_token

and

refresh_token

, which can be used for subsequent API calls.

Conclusion

The guide completes the integration of a Spring OAuth2 authorization server with Redis token storage, covering configuration, custom entities, security components, and end‑to‑end testing of all major grant types.

ClientDetails class diagram
ClientDetails class diagram
RedisTokenStore key generation
RedisTokenStore key generation
RedisTokenStore getName method
RedisTokenStore getName method
Token generation issue
Token generation issue
Debug view of token generation
Debug view of token generation
Data insertion screenshot
Data insertion screenshot
Data insertion screenshot 2
Data insertion screenshot 2
Authorization code request
Authorization code request
Login page after redirect
Login page after redirect
Successful login redirect
Successful login redirect
Authorization code received
Authorization code received
Password grant request
Password grant request
Password grant response
Password grant response
Client credentials flow
Client credentials flow
Implicit flow
Implicit flow
Refresh token flow
Refresh token flow
JavaRedisSpring BootsecurityOAuth2authorization-server
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.