1.项目结构
2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cxb</groupId>
<artifactId>oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-oauth2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 注意是starter,自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 不是starter,手动配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 将token存储在redis中 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.OAuth2ServerConfig
package com.cxb.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
public class OAuth2ServerConfig {
private static final String DEMO_RESOURCE_ID = "order";
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").authenticated();//配置order访问控制,必须认证过后才可以访问
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// password 方案一:明文存储,用于测试,不能用于生产
// String finalSecret = "123456";
// password 方案二:用 BCrypt 对密码编码
// String finalSecret = new BCryptPasswordEncoder().encode("123456");
// password 方案三:支持多种编码,通过密码的前缀区分编码方式
String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");
//配置两个客户端,一个用于password认证一个用于client认证
clients.inMemory().withClient("client_1")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret)
.and().withClient("client_2")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("oauth2")
// token失效的时间
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(7200)
.secret(finalSecret);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//允许表单认证
oauthServer.allowFormAuthenticationForClients();
}
}
}
4.SecurityConfiguration
package com.cxb.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder());
// }
@Bean
@Override
protected UserDetailsService userDetailsService(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// password 方案一:明文存储,用于测试,不能用于生产
// String finalPassword = "123456";
// password 方案二:用 BCrypt 对密码编码
// String finalPassword = bCryptPasswordEncoder.encode("123456");
// password 方案三:支持多种编码,通过密码的前缀区分编码方式
String finalPassword = "{bcrypt}"+bCryptPasswordEncoder.encode("123456");
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user_1").password(finalPassword).authorities("USER").build());
manager.createUser(User.withUsername("user_2").password(finalPassword).authorities("USER").build());
return manager;
}
/**
* springboot2.0 删除了原来的 plainTextPasswordEncoder
* https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/#10.3.2 DelegatingPasswordEncoder
*
*/
// password 方案一:明文存储,用于测试,不能用于生产
// @Bean
// PasswordEncoder passwordEncoder(){
// return NoOpPasswordEncoder.getInstance();
// }
// password 方案二:用 BCrypt 对密码编码
// @Bean
// PasswordEncoder passwordEncoder(){
// return new BCryptPasswordEncoder();
// }
// password 方案三:支持多种编码,通过密码的前缀区分编码方式,推荐
@Bean
PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
//
// /**
// * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
// */
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
// @formatter:on
}
}
5.IndexController
package com.cxb.oauth2.web;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("authentication1:" + authentication);
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("authentication2:" + authentication);
return "order id : " + id;
}
}
6.application.yml 这里将token存入到redis中,过期之后,需要重新获取token
server:
port: 8081
spring:
redis:
host: 127.0.0.1
database: 0
logging.level.org.springframework.security: DEBUG
7.启动类SpringbootOauth2Application
package com.cxb.oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootOauth2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootOauth2Application.class, args);
}
}
8.测试
1)先用密码模式获取token,这里headers里面的key和value在OAuth2ServerConfig文件里面配置的。
如图,获取了access_token,拿到token之后,才有权访问order的接口。
如果没有token直接访问order的接口,会报错,提示没有授权。
使用token访问:该token就是上面用密码模式获取的,访问成功。
2)使用client模式获取token。
访问接口成功。
token已经保存到了redis中,有效期内的t获取的token是不会改变的,过期之后,需要重新获取token。