前情提要
- 在上一节中,我们实现了用户的登录、注册、注销功能,并且解决了跨域问题。但是仍然有很多问题,比如
- 在我们成功登录之后,每一次访问需要authenticated()的接口时,都需要携带username和原来的password,这时会出现传输不安全的问题,username和password容易被拦截获取
- 即使没有上述问题,因为用的是Basic Auth认证模式,所以http请求头部的auth是不支持中文加密的,因此无法正常使用一个中文的用户名
- 后端并没有数据库匹配用户的登录状态,也不存在session,因此无法有效地判定用户令牌是否过期
- 不支持第三方登录,一个网站只能以一种方式登录,不方便
- 基于这些原因,我们需要一个功能更加强大的授权模式,这里选择了oauth2
什么是oauth2?
- 简单地来说就是一种认证标准,基于这种标准,可以实现通常所说的“第三方登录”,允许访问第三方服务器上的数据
oauth2的运作流程
- 客户端首先发送授权请求给用户,用户同意授权返回响应给客户端
- 客户端再发送授权请求给授权服务器,授权服务器同意授权返回token给客户端
- 客户端携带token发送获取资源的请求给资源服务器,资源服务器理解token并且返回受保护的资源给客户端
- 以下图出自简书
在项目中添加oauth2
- spring项目是集成了oauth2的,我们不需要自己实现授权逻辑与认证协议
- 在项目中添加oauth2依赖,此依赖比较特殊,不能在ui界面中添加
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
构建符合oauth2规范的数据表
- 根据文档指出,oauth2至少需要有一张表来存储client,一张表存储用户token,一张表存储refresh_token
- 官方给出的表结构如下
/* oauth部分 */
/* 客户端表,使用认证前需要向客户端表注册一个client */
CREATE TABLE oauth_client_details (
client_id varchar(48) NOT NULL,
resource_ids varchar(256) DEFAULT NULL,
client_secret varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
authorized_grant_types varchar(256) DEFAULT NULL,
web_server_redirect_uri varchar(256) DEFAULT NULL,
authorities varchar(256) DEFAULT NULL,
access_token_validity int DEFAULT NULL,
refresh_token_validity int DEFAULT NULL,
additional_information text DEFAULT NULL,
autoapprove varchar(256) DEFAULT NULL,
PRIMARY KEY (client_id)
);
/* access token */
CREATE TABLE oauth_access_token (
token_id varchar(256) DEFAULT NULL,
token blob,
authentication_id varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
client_id varchar(256) DEFAULT NULL,
authentication blob,
refresh_token varchar(256) DEFAULT NULL,
PRIMARY KEY (authentication_id)
);
/* 授权码 */
CREATE TABLE oauth_code (
code varchar(256) DEFAULT NULL,
authentication blob,
create_ts timestamp NULL DEFAULT CURRENT_TIMESTAMP
);
/* approval */
CREATE TABLE oauth_approvals (
userId varchar(256) DEFAULT NULL,
clientId varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
status varchar(10) DEFAULT NULL,
expiresAt datetime DEFAULT NULL,
lastModifiedAt datetime DEFAULT NULL
);
/* refresh token */
CREATE TABLE oauth_refresh_token (
create_time timestamp DEFAULT CURRENT_TIMESTAMP,
token_id varchar(256),
token blob,
authentication blob
);
CREATE INDEX token_id_index ON oauth_refresh_token (token_id);
配置oauth2的授权服务器
- 根据oauth2的认证流程就可以得知,用户就是我们自己,那么下一步就是配置一个授权服务器了,oauth2依赖中集成了授权服务器,在kmhc.config包中创建AuthorizationServerConfiguration类,代码如下
package kmhc.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import kmhc.security.CustomUserDetailsService;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// use jdbc service
clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
@Bean
@Primary
public AuthorizationServerTokenServices authorizationTokenService() {
// token service
DefaultTokenServices tokenServices = new DefaultTokenServices();
// jdbc client service
ClientDetailsService clientService = new JdbcClientDetailsService(dataSource);
tokenServices.setClientDetailsService(clientService);
tokenServices.setSupportRefreshToken(true);
// jdbc token store
TokenStore tokenStore = new JdbcTokenStore(dataSource);
tokenServices.setTokenStore(tokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setAccessTokenValiditySeconds(7200);
tokenServices.setRefreshTokenValiditySeconds(259200);
return tokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// User Details
endpoints.userDetailsService(userDetailsService);
// token management service
endpoints.tokenServices(authorizationTokenService());
// password mode need
endpoints.authenticationManager(authenticationManager);
// code mode need
endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource));
// allow post
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.POST);
// approval
ApprovalStore approvalStore = new JdbcApprovalStore(dataSource);
endpoints.approvalStore(approvalStore);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
- 上述代码难以理解的地方还是有很多的,希望大家可以通过查阅资料自行理解
- 注意到在@Autowired中注入了AuthenticationManager,虽然是springsecurity提供的,但是如果不显示地将它声明为@Bean并且返回是会报错的,所以需要在SecurityConfig类中加入一个@Bean
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
配置oauth2的资源服务器
- 在多服务器的情况下,资源服务器和授权服务器一般是分开的,但是我们只有自己的电脑,所以资源服务器也得配置为自己
- 同样是kmhc.config包,创建一个ResourceServerConfiguration类,代码如下
package kmhc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// the name of resource id
public static final String RESOURCE_ID = "kmhc";
@Bean
public ResourceServerTokenServices resourceTokenService() {
RemoteTokenServices service = new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://127.0.0.1:9001/oauth/check_token");
service.setClientId("kmhc");
service.setClientSecret("123456");
return service;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(resourceTokenService())
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/login").permitAll()
.antMatchers(HttpMethod.POST, "/api/users").permitAll()
.antMatchers(HttpMethod.GET, "/api/users/{username}").permitAll()
.antMatchers("/api/users/{username}").authenticated()
.antMatchers(HttpMethod.GET, "/api/users").hasRole("ADMIN")
.antMatchers("/api/authorities/**").hasRole("ADMIN")
.antMatchers("/api/groups/**").hasRole("ADMIN")
.antMatchers("/api/groupAuthorities/**").hasRole("ADMIN")
.and()
.formLogin().disable()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
- 在资源服务器的配置中,同样也有http的配置,这里的http配置会覆盖掉spring security的http配置
注册一个client
- 上文也说过了,想要使用oauth2的认证,必须先获得许可,即在oauth_client_details表中添加一条记录,输入如下命令添加一条记录
INSERT INTO oauth_client_details VALUES("kmhc", "kmhc", "$2a$10$wDk9AuORolw2M1486MReXuwMZbJRCrtvzH/EHopvk/8EufqaAts86", "all", "authorization_code,password,refresh_token", "http://127.0.0.1:9001/", "ROLE_USER", 7200, 86400, NULL, "TRUE");
用户名密码模式
- 现在已经配置好了oauth2的所有配置,可以用postman开始测试了,输入数据和输出数据如下
- 返回了这么一个json数据
{
"access_token": "2574b19e-36b6-411c-8446-7e16a9d2ed05",
"token_type": "bearer",
"refresh_token": "07ed80e9-b810-4a07-9dd2-9b3bafcdc47b",
"expires_in": 7199,
"scope": "all"
}
- 对json数据解析
- access_token是用户令牌,以后用户都需要在headers的Authorization中添加令牌来访问接口
- token_type是令牌类型,此处为bearer,在postman的Authorization中可以找到它,然后将access_token添加进去即可
- refresh_token用于刷新令牌的生效时间,access_token设置的有效期为2小时,refresh_token设置的有效期为24小时
- scope表示该令牌所能访问的应用范围,all表示整个网站都能访问
- 再看一下数据库的表,是否真的有数据,输入命令
select count(*) from oauth_access_token
,发现有1条数据在里面,说明授权成功
至此,oauth2的后端配置部分已经讲解完毕,下一节会讲解如何在前端使用oauth2来进行认证,进行令牌的交换