我理解的OAuth2.0
首先这是一种授权协议,个人理解这个协议最大的好处是可以使用第三方client进行登录,比如我们在登录csdn博客的时候,可以不必使用用户名密码,直接微信或者qq扫码登录即可。相比与普通的用户名密码登录授权,OAuth2.0多了一个client的概念,client即是你开发的网站,比如要接入微信的登录,你需要在微信网站应用开发创建审核相应的材料,然后获取APPId,此APPID即是client id,通过此client id,即可以使用微信的授权服务,获取token,进行网站的访问。
此处完全是我自己对于OAuth2的理解,如果不对之处,欢迎大佬批评指正。
OAuth2.0的四种授权模式
1. 授权码许可类型(Authorization Code)
2.隐式许可类型(Implicit)
3.客户端凭据许可(Client Credentials)
4.资源拥有者凭据许可(Resource Owner Password Credentials)
在这里我只说一下最复杂的授权码许可类型,其他的三种请参考OAuth 2.0 的四种方式。
授权码许可类型是最复杂的授权模式,其流程如下所示。登录客户端需要先获得授权码(authorization_code),然后利用code换取access_token。
代码讲解
总体概述
代码讲解分为两个部分,一个是授权服务,一个是资源服务,资源服务代码主要为了验证生成的access_token能访问相应的资源。
整个项目的pom文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hz.oauth</groupId>
<artifactId>authority-management-sys-2.0</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>
<modules>
<module>oauth2-resource</module>
<module>oauth2-server</module>
</modules>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<org.apache.common.lang3.version>3.12.0</org.apache.common.lang3.version>
<spring-framework.cloud.version>Greenwich.SR4</spring-framework.cloud.version>
<org.apache.common.beanutils.version>1.9.4</org.apache.common.beanutils.version>
<com.baomidou.mybatis-plus.version>3.4.3</com.baomidou.mybatis-plus.version>
<mysql.connector.java.version>8.0.12</mysql.connector.java.version>
<com.alibaba.fastjson.version>1.2.76</com.alibaba.fastjson.version>
<!--Lombok-->
<lombok.version>1.18.10</lombok.version>
<commons-io.version>2.6</commons-io.version>
<javadoc.version>3.0.0</javadoc.version>
<maven-release-plugin.version>2.5.3</maven-release-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-framework.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${org.apache.common.lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${org.apache.common.beanutils.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${com.baomidou.mybatis-plus.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${com.alibaba.fastjson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- for create custom archetype template -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-archetype-plugin</artifactId>
<version>3.0.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
oauth2-server
最为重要的是两个配置文件类,一个是授权服务的配置,一个是security的安全服务配置,security的安全服务配置重要的是把授权服务相应的地址配置允许匿名访问。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.hz.oauth</groupId>
<artifactId>authority-management-sys-2.0</artifactId>
<version>1.0</version>
</parent>
<artifactId>oauth2-server</artifactId>
<modelVersion>4.0.0</modelVersion>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- for create custom archetype template -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-archetype-plugin</artifactId>
<version>3.0.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/*.jks</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/*.jks</include>
</includes>
</resource>
</resources>
</build>
</project>
SysAuthorizationConfig
在该类配置中,授权码的保存,用户的授权记录保存在数据库中,其生成数据库代码为
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`partnerKey` varchar(32) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
SysAuthorizationConfig类配置代码
/**
* @program: OAuth-2.0
* @author: zgr
* @create: 2021-07-20 11:42
**/
@Configuration
@EnableAuthorizationServer
public class SysAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final SysClientService sysClientService;
private final AuthenticationManager authenticationManager;
public SysAuthorizationConfig(DataSource dataSource, SysClientService sysClientService, AuthenticationManager authenticationManager) {
this.dataSource = dataSource;
this.sysClientService = sysClientService;
this.authenticationManager = authenticationManager;
}
/**
* 维护一套客户端的信息 即第三方应用软件申请时记录的一些基本信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(sysClientService);
}
/**
* 打开验证token的访问权限
*
* @param security
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
.passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 配置了令牌存储方式为jwt
* 配置JWT Token的非对称加密来进行签名
* 配置一个自定义的Token增强器,把更多信息放入Token中
* 配置使用JDBC数据库方式来保存用户的授权批准记录
*
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtTokenEnhancer()));
endpoints.approvalStore(approvalStore())
.authorizationCodeServices(authorizationCodeServices())
.tokenEnhancer(tokenEnhancerChain)
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
/**
* 使用数据库方式来保存授权码
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 采用jwt方式存储token
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
/**
* 使用数据库方式存储用户的批准授权记录
*
* @return
*/
@Bean
public JdbcApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
/**
* 自定义token增强器
*
* @return
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return new SysTokenEnhancer();
}
/**
* jwt 验证
*
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 设置jwt加解密秘钥,不设置会随机一个
jwtAccessTokenConverter.setSigningKey(Constant.JWT_SECRET);
return jwtAccessTokenConverter;
}
}
SysSecurityConfig
**
* @program: authority-management-sys-2.0
* @author: zgr
* @create: 2021-08-03 09:55
**/
@Configuration
@EnableWebSecurity
public class SysSecurityConfig extends WebSecurityConfigurerAdapter {
private final SysUserService sysUserService;
private final SysLogoutHandler sysLogoutHandler;
private final SysLogoutSuccessHandler sysLogoutSuccessHandler;
public SysSecurityConfig(SysUserService sysUserService, SysLogoutHandler sysLogoutHandler, SysLogoutSuccessHandler sysLogoutSuccessHandler) {
this.sysUserService = sysUserService;
this.sysLogoutHandler = sysLogoutHandler;
this.sysLogoutSuccessHandler = sysLogoutSuccessHandler;
}
/**
* 配置认证管理器
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 这里是对认证管理器的添加配置,自定义用户详情
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(sysUserService).passwordEncoder(new BCryptPasswordEncoder())
;
}
//web ignore比较适合配置前端相关的静态资源,它是完全绕过spring security的所有filter的
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resource");
}
/**
* 安全请求配置,这里配置的是security的部分,这里配置全部通过,安全拦截在资源服务的配置文件中配置,
* 要不然访问未验证的接口将重定向到登录页面,前后端分离的情况下这样并不友好,无权访问接口返回相关错误信息即可
*
* @param http
* @return void
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginPage("/login").and()
.logout().addLogoutHandler(sysLogoutHandler).logoutSuccessHandler(sysLogoutSuccessHandler)
.and()
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable().cors().and()
.authorizeRequests()
.antMatchers("/login", "/oauth/authorize").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
客户端服务ClientDetailsService
在SysAuthorizationConfig中,我们配置的是客户端 服务为自定义的sysClientService。实际应用中,通过client id查出对应客户端的信息,查询其拥有的resource资源权限,生成access_token。
/**
* 维护一套客户端的信息 即第三方应用软件申请时记录的一些基本信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(sysClientService);
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SysClientDetails implements ClientDetails {
private String clientId;
private Set<String> resourceId;
private Boolean isSecretRequired;
private String clientSecret;
private Boolean isScoped;
private Set<String> scope;
private Set<String> authorizedGrantTypes;
private Set<String> registeredRedirectUri;
private Integer accessTokenValiditySeconds;
private Integer refreshTokenValiditySeconds;
private Boolean isAutoApprove;
private Map<String, Object> additionalInformation;
@Override
public String getClientId() {
return this.clientId;
}
@Override
public Set<String> getResourceIds() {
return this.resourceId;
}
@Override
public boolean isSecretRequired() {
return this.isSecretRequired;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public boolean isScoped() {
return this.isScoped;
}
@Override
public Set<String> getScope() {
return this.scope;
}
@Override
public Set<String> getAuthorizedGrantTypes() {
return this.authorizedGrantTypes;
}
@Override
public Set<String> getRegisteredRedirectUri() {
return this.registeredRedirectUri;
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public Integer getAccessTokenValiditySeconds() {
return this.accessTokenValiditySeconds;
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return this.refreshTokenValiditySeconds;
}
@Override
public boolean isAutoApprove(String scope) {
return this.isAutoApprove;
}
@Override
public Map<String, Object> getAdditionalInformation() {
return this.additionalInformation;
}
}
/**
* @program: authority-management-sys-2.0
* @author: zgr
* @create: 2021-08-03 10:12
**/
@Service
@Slf4j
public class SysClientServiceImpl implements SysClientService {
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
log.info("查找客户端id为 {} 的客户端详情", clientId);
//实际使用中可以从数据库查询客户端详细信息,本例子中直接构造一个客户端
Set<String> resourceIds = new HashSet<>();
resourceIds.add("userservice1");
Set<String> scopes = new HashSet<>(1);
scopes.add("sever");
Set<String> grantTypes = new HashSet<>(5);
grantTypes.add("password");
grantTypes.add("authorization_code");
Set<String> registeredRedirectUris = new HashSet<>(1);
registeredRedirectUris.add("https://baidu.com");
return SysClientDetails.builder()
//客户端id
.clientId("userservice1")
//拥有的资源id
.resourceId(resourceIds)
//是否需要secret
.isSecretRequired(true)
//secret加密方式
.clientSecret(new BCryptPasswordEncoder().encode("1234"))
//权限范围
.scope(scopes)
//支持的授权模式,四种模式支持哪几种
.authorizedGrantTypes(grantTypes)
//官方注册的回调地址,这个地址是需要和授权请求的回调地址一致
.registeredRedirectUri(registeredRedirectUris)
.isScoped(true)
//access_token的有效时间
.accessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(2))
//refresh_token的有效时间
.refreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30))
//是否可以自主授权
.isAutoApprove(true)
.build();
}
}
postman演示结果
1.资源有着凭据许可
完整的返回数据:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidXNlcnNlcnZpY2UxIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsic2V2ZXIiXSwiZXhwIjoxNjI4NzU3NTMzLCJ1c2VyRGV0YWlscyI6eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjpudWxsLCJyb2xlcyI6W3siaWQiOjEsIm5hbWUiOiJSRUFEIn0seyJpZCI6MSwibmFtZSI6IldSSVRFIn1dLCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUkVBRCJ9LHsiYXV0aG9yaXR5IjoiV1JJVEUifV0sImVuYWJsZWQiOnRydWUsImNyZWRlbnRpYWxzTm9uRXhwaXJlZCI6dHJ1ZSwiYWNjb3VudE5vbkV4cGlyZWQiOnRydWUsImFjY291bnROb25Mb2NrZWQiOnRydWV9LCJhdXRob3JpdGllcyI6WyJSRUFEIiwiV1JJVEUiXSwianRpIjoiODAwZjgwMmUtZTUzNS00N2RiLTkzOTUtZjY5ZmQxM2Q3ZjNjIiwiY2xpZW50X2lkIjoidXNlcnNlcnZpY2UxIn0.N8HLGnDbRiN560SuE_a4IlqEZIk3yCpzpyEXH3VDb1w",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidXNlcnNlcnZpY2UxIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsic2V2ZXIiXSwiYXRpIjoiODAwZjgwMmUtZTUzNS00N2RiLTkzOTUtZjY5ZmQxM2Q3ZjNjIiwiZXhwIjoxNjMxMTc2NzMyLCJ1c2VyRGV0YWlscyI6eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjpudWxsLCJyb2xlcyI6W3siaWQiOjEsIm5hbWUiOiJSRUFEIn0seyJpZCI6MSwibmFtZSI6IldSSVRFIn1dLCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUkVBRCJ9LHsiYXV0aG9yaXR5IjoiV1JJVEUifV0sImVuYWJsZWQiOnRydWUsImNyZWRlbnRpYWxzTm9uRXhwaXJlZCI6dHJ1ZSwiYWNjb3VudE5vbkV4cGlyZWQiOnRydWUsImFjY291bnROb25Mb2NrZWQiOnRydWV9LCJhdXRob3JpdGllcyI6WyJSRUFEIiwiV1JJVEUiXSwianRpIjoiZDMyNGQ4ZjEtMjBiMy00MTg2LTk4NzMtZjg2NDhkNjI1ZTc5IiwiY2xpZW50X2lkIjoidXNlcnNlcnZpY2UxIn0.KbqODRoK0hjrrHWeKvtZ2HNkcREPFmuv7yvOrZGzEnE",
"expires_in": 172799,
"scope": "sever",
"userDetails": {
"id": 1,
"username": "admin",
"password": null,
"roles": [
{
"id": 1,
"name": "READ"
},
{
"id": 1,
"name": "WRITE"
}
],
"authorities": [
{
"authority": "READ"
},
{
"authority": "WRITE"
}
],
"enabled": true,
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
},
"jti": "800f802e-e535-47db-9395-f69fd13d7f3c"
}
2.授权码模式
在浏览器输入地址http://localhost:8086/api/v1/oauth/authorize?response_type=code&client_id=userservice1&redirect_uri=https://baidu.com,会跳转到授权页面,授权页面利用spring mvc定义了一个页面放在resources目录下。
<!DOCTYPE html>
<html class="uk-height-1-1" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>OAuth2 Demo</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.26.3/css/uikit.gradient.min.css" rel="stylesheet"/>
</head>
<body class="uk-height-1-1">
<div class="uk-vertical-align uk-text-center uk-height-1-1">
<div class="uk-vertical-align-middle" style="width: 250px;">
<h1>Login Form</h1>
<p class="uk-text-danger" th:if="${param.error}">
用户名或密码错误...
</p>
<form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/login}">
<div class="uk-form-row">
<input class="uk-width-1-1 uk-form-large" name="username" placeholder="Username" type="text"
value="admin"/>
</div>
<div class="uk-form-row">
<input class="uk-width-1-1 uk-form-large" name="password" placeholder="Password" type="password"
value="admin"/>
</div>
<div class="uk-form-row">
<button class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Login</button>
</div>
</form>
</div>
</div>
</body>
</html>
跳转到登录页面,点击登录,登录成功后,跳转到百度页面,并得到code授权码
拿到获取的code,生成access_token
完整的返回信息
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidXNlcnNlcnZpY2UxIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsic2V2ZXIiXSwiZXhwIjoxNjI4NzU4Mjc2LCJ1c2VyRGV0YWlscyI6eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjpudWxsLCJyb2xlcyI6W3siaWQiOjEsIm5hbWUiOiJSRUFEIn0seyJpZCI6MSwibmFtZSI6IldSSVRFIn1dLCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUkVBRCJ9LHsiYXV0aG9yaXR5IjoiV1JJVEUifV0sImVuYWJsZWQiOnRydWUsImNyZWRlbnRpYWxzTm9uRXhwaXJlZCI6dHJ1ZSwiYWNjb3VudE5vbkV4cGlyZWQiOnRydWUsImFjY291bnROb25Mb2NrZWQiOnRydWV9LCJhdXRob3JpdGllcyI6WyJSRUFEIiwiV1JJVEUiXSwianRpIjoiZGM5Y2FjN2UtN2M3My00YThkLWEzMzYtZTI2ZmNjYzllODE0IiwiY2xpZW50X2lkIjoidXNlcnNlcnZpY2UxIn0.sOhH-eFcldWnnOfZmDqyuPkl4Qkg8LyJnPZ0gwubBWc",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidXNlcnNlcnZpY2UxIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsic2V2ZXIiXSwiYXRpIjoiZGM5Y2FjN2UtN2M3My00YThkLWEzMzYtZTI2ZmNjYzllODE0IiwiZXhwIjoxNjMxMTc3NDc2LCJ1c2VyRGV0YWlscyI6eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInBhc3N3b3JkIjpudWxsLCJyb2xlcyI6W3siaWQiOjEsIm5hbWUiOiJSRUFEIn0seyJpZCI6MSwibmFtZSI6IldSSVRFIn1dLCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUkVBRCJ9LHsiYXV0aG9yaXR5IjoiV1JJVEUifV0sImVuYWJsZWQiOnRydWUsImNyZWRlbnRpYWxzTm9uRXhwaXJlZCI6dHJ1ZSwiYWNjb3VudE5vbkV4cGlyZWQiOnRydWUsImFjY291bnROb25Mb2NrZWQiOnRydWV9LCJhdXRob3JpdGllcyI6WyJSRUFEIiwiV1JJVEUiXSwianRpIjoiNDdmYjUzMDctYzcyYy00ZjI1LWI4MjEtNTY2MGNlYWYyZGMwIiwiY2xpZW50X2lkIjoidXNlcnNlcnZpY2UxIn0.vmtnmLWsaGBxFxAZ-0nsETBUsZ3muSFKqjffAweRZTQ",
"expires_in": 172799,
"scope": "sever",
"userDetails": {
"id": 1,
"username": "admin",
"password": null,
"roles": [
{
"id": 1,
"name": "READ"
},
{
"id": 1,
"name": "WRITE"
}
],
"authorities": [
{
"authority": "READ"
},
{
"authority": "WRITE"
}
],
"enabled": true,
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
},
"jti": "dc9cac7e-7c73-4a8d-a336-e26fccc9e814"
}
oauth2-resource
oauth2-resource为资源,用户在获得access_token后,携带access_token访问资源。最重要的是资源配置类。
@Configuration
//启用资源服务器
@EnableResourceServer
//启用方法注解方式来进行权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 声明了资源服务器的ID是userservice,声明了资源服务器的TokenStore是JWT
* 注意,这里硬编码了资源id,这个资源id和client所拥有的的资源id需要匹配
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("userservice1").tokenStore(tokenStore());
}
/**
* 配置TokenStore
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置解析
*
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//和授权服务的key相同
converter.setSigningKey(Constant.JWT_SECRET);
return converter;
}
}
security配置接口访问权限
@Configuration
@EnableWebSecurity
public class ResourceSecurityConfig extends WebSecurityConfigurerAdapter {
//web ignore比较适合配置前端相关的静态资源,它是完全绕过spring security的所有filter的
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resource");
}
/**
* 安全请求配置,这里配置的是security的部分,这里配置全部通过,安全拦截在资源服务的配置文件中配置,
* 要不然访问未验证的接口将重定向到登录页面,前后端分离的情况下这样并不友好,无权访问接口返回相关错误信息即可
*
* @param http
* @return void
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll();
}
}
ctrl接口测试类
**
* @author honorzhang
*/
@RestController
@RequestMapping("/user")
public class ResourceUserController {
@Autowired
private TokenStore tokenStore;
/***
* 读权限或写权限可访问,返回登录用户名
* @param authentication
* @return
*/
@PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')")
@GetMapping("/name")
public BaseResponse<String> name(OAuth2Authentication authentication) {
return BaseResponse.success(authentication.getName());
}
/**
* 读权限或写权限可访问,返回登录用户信息
*
* @param authentication
* @return
*/
@PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')")
@GetMapping
public BaseResponse<OAuth2Authentication> read(OAuth2Authentication authentication) {
return BaseResponse.success(authentication);
}
/**
* 只有写权限可以访问,返回访问令牌中的额外信息
*
* @param authentication
* @return
*/
@PreAuthorize("hasAuthority('WRITE') and hasAuthority('ADMIN')")
@PostMapping
public BaseResponse<Object> write(OAuth2Authentication authentication) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(details.getTokenValue());
return BaseResponse.success(accessToken.getAdditionalInformation().getOrDefault("userDetails", null));
}
}
postman结果测试
access_token 权限包含WRITE 和 READ权限,没有ADMIN权限。
访问read接口,权限通过。
访问write接口,没有ADMIN权限,不能访问
引用
写在最后
本文是根据极客时间的课程,利用spring cloud oauth2 实现的OAuth2.0权限控制。有很多是基于自己的理解,如有不正确之处,请互相指教。