Master OAuth2: From Theory to Hands‑On Implementation with Spring Security
This article explains OAuth2 fundamentals, key terminology, and authorization flows, then guides you through setting up database tables, Spring Boot dependencies, resource and authorization server configurations, multiple grant types, token refresh, permission checks, and common pitfalls, providing complete code snippets and diagrams for a practical implementation.
Theory
OAuth is an open standard for authorization that enables third‑party applications to obtain limited access to user data. The current version is 2.0.
Application Scenario
When a user clicks “like” on a post in a website, the system may prompt a login page offering password login or quick login via Weibo, WeChat, QQ, etc. After successful quick login, the user’s nickname and avatar are returned to the site, allowing the like action.
Key Terms
Client: the application requesting resources (e.g., the website).
Resource Owner: the user who owns the data.
Authorization Server: issues and validates token s.
Resource Server: stores the protected resources.
Authorization Flow
The standard OAuth2 flow consists of:
Client requests authorization from the resource owner.
Resource owner grants permission and returns an authorization code.
Client exchanges the code for a token at the authorization server.
Authorization server validates the client and issues the token.
Client uses the token to request resources from the resource server.
Resource server validates the token and returns the requested data.
Hands‑On
Before coding, create the necessary OAuth2 tables in the database. The init.sql file defines tables such as oauth_client_details , oauth_access_token , oauth_client_token , oauth_code , oauth_approvals , and oauth_refresh_token . Example client insertion:
<code>client_id:cheetah_one // client name, must be unique
resource_ids:product_api // resources the client can access
client_secret:$2a$10$h/TmLPvXozJJHXDyJEN22ensJgaciomfpOc9js9OonwWIdAnRQeoi // client password
scope:read,write // permission scopes
authorized_grant_types:client_credentials,implicit,authorization_code,refresh_token,password // supported grant types
web_server_redirect_uri:http://www.baidu.com // redirect URI (optional for certain grant types)
access_token_validity:43200 // token lifetime in seconds
autoapprove:false // whether user auto‑approves</code>Add the required Maven dependencies:
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency></code>Resource Server
Configure the server port, application name, datasource, MyBatis, and logging. A simple controller demonstrates protected resource access:
<code>@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping("/findAll")
public String findAll() {
return "产品列表查询成功";
}
}</code>Extend ResourceServerConfigurerAdapter and enable the resource server with @EnableResourceServer . Override the token store and HTTP security to enforce scope‑based access and handle CORS:
<code>@Bean
public TokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("product_api").tokenStore(jdbcTokenStore());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.and()
.headers().addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Allow-Methods"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Allow-Headers"));
}
});
}
</code>Authorization Server
Enable the server with @EnableAuthorizationServer and inject required beans such as DataSource , ISysUserService , and AuthenticationManager . Define beans for client details, token store, approval store, and authorization code services:
<code>@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
</code>Configure client details, token security, and endpoint behavior:
<code>@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.userDetailsService(userService)
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
</code>Grant Types
Authorization Code (most secure): two‑step flow with a one‑time code.
Implicit (simplified): token returned directly in the redirect URI; not recommended due to exposure.
Password : client receives user credentials; requires high trust.
Client Credentials : client authenticates without a user; used for service‑to‑service calls.
Token Refresh
Use the refresh_token grant to obtain a new access token when the original expires.
Permission Checks
Enable method‑level security with @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) . Annotate controller methods with @Secured("ROLE_PRODUCT") and assign roles in loadUserByUsername :
<code>@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
sysUser.setRoleList(AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_PRODUCT"));
return sysUser;
}
</code>Common Pitfalls
Inconsistent package names between modules cause deserialization failures when the token is read from oauth_access_token .
Missing or incorrect role strings in the database lead to “access denied” errors; clearing and re‑creating token tables can resolve the issue.
Conclusion
The article covered OAuth2 concepts, application scenarios, the full authorization flow, database schema, Spring Boot configuration for both resource and authorization servers, all four grant types, token refresh, dynamic permission checks, and troubleshooting tips, providing a complete hands‑on guide.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
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.