参考链接:
Oauth2.0简单解释
Oauth2.0四种方式
什么是JWT
JWT无状态登录
Spring security 系列15篇
Spring boot security 学习
Spring Security Oauth2 permitAll()还校验token
源码分析
UsernamePasswordAuthenticationFilter
前言
网上oauth2相关的demo讲的很笼统,几乎都是内存配置的方式简单演示了一下。
这段时间踩了很多坑,因此整理出了这篇文章
本文解决了如下问题:
- 前后端分离方式,进行oauth2配置,登录成功失败等全部处理,全部返回json
- 使用oauth的数据库方式配置
- 去除框架中的权限判断默认前缀 ROLE_
- 跨域问题,以及需要放行option请求
- jwt方式存储token,登出后token仍旧有效,采用登录成功时将token保存在redis中,登出或修改密码等操作删除redis中的token。并添加jwt过滤器,每次访问接口对token进行校验
- 资源服务器调用check_token接口,服务器直接500了,其实只是授权服务器发现token无效返回了400
未解决的问题:
1. 使用nginx做了负载均衡,搭建了两台资源服务器和两台认证服务器A B,登录资源服务器登录时调用了认证服务器A,获取授权码时(/oauth/authorize)调用了认证服务器B,返回401,尚不知道如何解决?
背景
客户使用多个系统,每个系统都有自己的账号密码,想通过我们的门户系统进行登录授权(其他系统登需要登录时,跳转门户进行登录授权)
服务器
前端服务器
资源服务器(子系统)
认证服务器
认证服务器和资源服务器分为两个项目(模块)
security oauth2框架的交互手段
前端与资源服务器交互(是否登录过):通过session交互
前端与资源服务器交互(每次都会调用认证服务器的/check_token):通过token(header中的Authorization属性)交互
认证流程
流程demo
- 前端服务器访问后端(资源服务器) 活动报名接口(/active)
- 资源服务器会调用认证服务器的/check_token接口,进行校验header中的token校验
- 校验不通过,返回401,前端跳转到登录页面。进行登录
- 登录成功,前端调用 http://127.0.0.1:8080/oauth/authorize?response_type=code&client_id=website&redirect_uri=http://192.168.10.182:8008/web-portal/su/token&scope=all 进行授权(自动授权)
- 认证成功,认证服务器回调: http://127.0.0.1:8008/web-portal/su/token?code=授权码
- 前端拿到返回的token信息,放到header中,与子系统进行交互
依赖问题
最初使用spring boot oauth2依赖有问题(据说不再维护)
因此采用spring-cloud-starter-oauth2的依赖
文章只提供oauth2 jwt相关的依赖
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
数据库配置
表含义这里不多做介绍了,都是security oauth2框架使用数据库模式需要的表,认证配置的表是:oauth_client_details ,需要在该表中配置客户端id,回调URL等信息
CREATE TABLE oauth_access_token (
token_id VARCHAR(256) NULL DEFAULT NULL,
token TEXT NULL DEFAULT NULL,
authentication_id VARCHAR(128) NOT NULL PRIMARY KEY,
user_name VARCHAR(256) NULL DEFAULT NULL,
client_id VARCHAR(256) NULL DEFAULT NULL,
authentication text NULL DEFAULT NULL,
refresh_token VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_access_token OWNER to dna_portal;
CREATE TABLE oauth_approvals (
userId VARCHAR(256) NULL DEFAULT NULL,
clientId VARCHAR(256) NULL DEFAULT NULL,
scope VARCHAR(256) NULL DEFAULT NULL,
status VARCHAR(10) NULL DEFAULT NULL,
expiresAt time NULL DEFAULT NULL,
lastModifiedAt time NULL DEFAULT NULL);
ALTER TABLE public.oauth_approvals OWNER to dna_portal;
CREATE TABLE oauth_client_details (
client_id VARCHAR(128) NOT NULL PRIMARY KEY,
resource_ids VARCHAR(256) NULL DEFAULT NULL,
client_secret VARCHAR(256) NULL DEFAULT NULL,
scope VARCHAR(256) NULL DEFAULT NULL,
authorized_grant_types VARCHAR(256) NULL DEFAULT NULL,
web_server_redirect_uri VARCHAR(256) NULL DEFAULT NULL,
authorities VARCHAR(256) NULL DEFAULT NULL,
access_token_validity INT8 NULL DEFAULT NULL,
refresh_token_validity INT8 NULL DEFAULT NULL,
additional_information VARCHAR(4096) NULL DEFAULT NULL,
autoapprove VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_client_details OWNER to dna_portal;
insert into oauth_client_details (client_id,resource_ids,client_secret,scope,authorized_grant_types,
web_server_redirect_uri,authorities,access_token_validity,refresh_token_validity,additional_information,autoapprove) values
('website', 'website', '$2a$10$.ebjcgCVOHuEscJ6xLyQcu21nW93XuHZ2qk2TRbTofDLVhPY0C5S2', 'all', 'authorization_code,refresh_token',
'http://127.0.0.1:8080/web-portal/su/token', 'admin,ROLE_admin',36000, null, null, true);
CREATE TABLE oauth_client_token (
token_id VARCHAR(256) NULL DEFAULT NULL,
token text NULL DEFAULT NULL,
authentication_id VARCHAR(128) NOT NULL PRIMARY KEY,
user_name VARCHAR(256) NULL DEFAULT NULL,
client_id VARCHAR(256) NULL DEFAULT NULL);
ALTER TABLE public.oauth_client_token OWNER to dna_portal;
CREATE TABLE oauth_code (
code VARCHAR(256) NULL DEFAULT NULL,
authentication text NULL DEFAULT NULL);
ALTER TABLE public.oauth_code OWNER to dna_portal;
CREATE TABLE oauth_refresh_token (
token_id VARCHAR(256) NULL DEFAULT NULL,
token text NULL DEFAULT NULL,
authentication text NULL DEFAULT NULL);
ALTER TABLE public.oauth_refresh_token OWNER to dna_portal;
资源服务器配置
跨域配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 跨域访问配置
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
资源服务器配置
# 本应用占用端口
server:
port: 8008
servlet:
context-path: /web-portal
session:
cookie:
name: OAUTH2-PORTAL-SESSIONID
oauth2:
server:
# oauth_client_details配置的client_id
clientId: website
# oauth_client_details配置的client_secret(明文)
clientSecret: 2020
# oauth2-server服务的地址
tokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/token
checkTokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/check_token
# jwt密钥
jwtSecret: drinkless-jwt-key
import com.drinkless.portal.filter.JWTAuthenticationFilter;
import com.drinkless.portal.handlder.ApiAccessDeniedHandler;
import com.drinkless.portal.handlder.AuthExceptionEntryPoint;
import com.drinkless.portal.handlder.CustomerResponseErrorHandler;
import com.drinkless.portal.handlder.LoginExpireHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
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.core.GrantedAuthorityDefaults;
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.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.client.RestTemplate;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${oauth2.server.checkTokenAddr}")
private String checkTokenAddr;
@Value("${oauth2.server.clientId}")
private String clientId;
@Value("${oauth2.server.clientSecret}")
private String clientSecret;
@Value("${oauth2.server.jwtSecret}")
private String jwtSecret;
//放行接口 swagger,系统连接性检查 和一些不需要登录就可访问的接口
public static String[] passUrl = {
"/su/token/**",
"/region/getTree",
"/app/status",
"/sd/getDictList/**",
"/version",
"/article/getList",
"/article/find/**",
"/advertisement/list",
"/category/getCategoryList",
"/category/find/**",
"/advertisement/list",
"/_health",
"/v2/api-docs", "/swagger-resources/configuration/ui","/swagger-resources", "/swagger-resources/configuration/security",
"/swagger-ui.html","/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico"};
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder builder){
RestTemplate restTemplate = builder.build();
/*为RestTemplate配置异常处理器0*/
restTemplate.setErrorHandler(new CustomerResponseErrorHandler());
return restTemplate;
}
//不使用权限校验的ROLE_前缀 (http.servletApi().rolePrefix("");该方式无效果,使用@Bean方式)
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
@Autowired
private RestTemplate restTemplate;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(clientId);
resources.tokenStore(new JwtTokenStore(accessTokenConverter())).stateless(true);
/* 配置令牌验证 */
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
remoteTokenServices.setRestTemplate(restTemplate);
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenAddr);
remoteTokenServices.setClientId(clientId);
remoteTokenServices.setClientSecret(clientSecret);
resources.tokenServices(remoteTokenServices).stateless(true);
//check_token异常类
resources.authenticationEntryPoint(new AuthExceptionEntryPoint());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(jwtSecret);
return converter;
}
/* 配置资源拦截规则 */
@Override
public void