Build an OAuth2 Authorization Server with Spring Boot, Redis, and MySQL
This tutorial walks through creating a full‑featured OAuth2 authorization server using Spring Boot 2.4.12, storing tokens in Redis, defining client and user entities in MySQL, configuring grant types, and testing the authorization, password, client‑credentials, and implicit flows.
This guide demonstrates how to build an OAuth2 authorization server with Spring Boot 2.4.12, using Redis for token storage and MySQL for client and user data.
Project Dependencies (pom.xml)
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.2.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency></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;
/** client ID */
private String clientId;
/** client secret */
private String clientSecret;
/** redirect URI */
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>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 (OAuthAuthorizationConfig)
<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 translator
endpoints.exceptionTranslator(new DefaultWebResponseExceptionTranslator() {
@Override
public ResponseEntity translate(Exception e) throws Exception {
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
return exceptionProcess(responseEntity);
}
});
endpoints.authenticationManager(authenticationManager);
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());
clientDetails.setRegisteredRedirectUri(Set.of(app.getRedirectUri()));
clientDetails.setScoped(false);
clientDetails.setSecretRequired(true);
clientDetails.setScope(Set.of());
clientDetails.setAuthorizedGrantTypes(Set.of("authorization_code", "implicit", "password", "refresh_token", "client_credentials"));
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;
};
}
@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
<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(),
List.of(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>Grant Types Overview
Authorization Code Grant : The most secure flow; the client obtains an authorization code via a backend server and exchanges it for an access token.
Password Grant : The resource owner provides username and password directly to the client; suitable only for highly trusted clients.
Client Credentials Grant : The client authenticates itself to obtain an access token without user involvement.
Implicit Grant : Tokens are returned directly in the browser fragment, bypassing the authorization code step.
Refresh Token : Allows the client to obtain a new access token before the current one expires.
Testing Steps
1. Insert client and user records into the MySQL tables. 2. Access the authorization endpoint to obtain an authorization code, then exchange it for a token. 3. Use the password grant endpoint with username and password to receive a token. 4. Verify client‑credentials and implicit flows similarly. 5. Use the returned refresh_token to get a new access token when needed.
All steps are illustrated with screenshots in the original article.
End of tutorial.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.