https://www.processon.com/view/link/60a32e7a079129157118740f
微信开发平台文档:
https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope)。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。
不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
适用场景:目前市面上主流的第三方验证都是采用这种模式
它的步骤如下:
用户 — JD – 微信
https://b.com/oauth/authorize?
response_type=code& #表示授权类型,必选项,此处的值固定为"code"
client_id=CLIENT_ID& #表示客户端的ID,必选项
redirect_uri=CALLBACK_URL& #表示重定向URI,可选项
scope=read& #表示申请的权限范围,可选项
state=STATE #表示客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值。
http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
https://a.com/callback?code=AUTHORIZATION_CODE #code参数就是授权码
https://www.baidu.com/?code=f1He3s
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET& # client_id和client_secret用来让 B 确认 A 的身份,client_secret参数是保密的,因此只能在后端发请求
grant_type=authorization_code& # 采用的授权方式是授权码
code=AUTHORIZATION_CODE& # 上一步拿到的授权码
redirect_uri=CALLBACK_URL # 令牌颁发后的回调网址
curl --location --request POST 'http://localhost:9999/oauth/token' \
--form 'redirect_uri="http://www.baidu.com"' \
--form 'code="f1He3s"' \
--form 'grant_type="authorization_code"' \
--form 'client_id="client"' \
--form 'client_secret="123123"'
{
"access_token": "3d80af21-a204-45e9-9bb1-5f9237aad88b", # 令牌
"token_type": "bearer",
"refresh_token": "c016714f-d376-417c-bea1-4d82f37c5b74",
"expires_in": 3599,
"scope": "all"
}
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)
适用场景:纯前端应用,没有后端
https://b.com/oauth/authorize?
response_type=token& # response_type参数为token,表示要求直接返回令牌
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
https://a.com/callback#token=ACCESS_TOKEN #token参数就是令牌,A 网站直接在前端拿到令牌。
https://www.baidu.com/#access_token=5c9273d1-55ad-4bc8-b928-ec037549a571&token_type=bearer&expires_in=3599
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
适用场景:自家公司搭建的授权服务器
https://oauth.b.com/token?
grant_type=password& # 授权方式是"密码式"
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
client_secret=client_secret
http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
{
access_token: "5c9273d1-55ad-4bc8-b928-ec037549a571",
token_type: "bearer",
refresh_token: "c016714f-d376-417c-bea1-4d82f37c5b74",
expires_in: 3069,
scope: "all",
}
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行授权。
适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
{
access_token: "c052012f-d260-49b2-b78d-774f44963914",
token_type: "bearer",
expires_in: 3599,
scope: "all",
}
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了
Header 加 Authorization
curl -H "Authorization: Bearer ACCESS_TOKEN" "https://api.b.com"
也可以通过添加请求参数access_token请求数据
http://localhost:8080/user/getCurrentUser?access_token=3d80af21-a204-45e9-9bb1-5f9237aad88b
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token?
grant_type=refresh_token& # grant_type参数为refresh_token表示要求更新令牌
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN # 用于更新令牌的令牌
http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=c016714f-d376-417c-bea1-4d82f37c5b74
{
access_token: "ac892fe7-8890-414f-8497-e85c3e6d7e49",
token_type: "bearer",
refresh_token: "cf749209-3ed7-48cf-a4e3-2e0de33624e8",
expires_in: 3599,
scope: "all",
}
将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。我们可以通过Spring Security OAuth2构建一个授权服务器来验证用户身份以提供access_token,并使用这个access_token来从资源服务器请求数据。
流程:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.3.4.RELEASEversion>
dependency>
或者 引入spring cloud oauth2依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
dependencies>
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
//获取用户信息
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.formLogin().permitAll()
.and().authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/order/**").permitAll()
.anyRequest().authenticated()
.and().logout().permitAll()
.and().csrf().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
UserService
@Service
public class UserService implements UserDetailsService {
@Autowired
@Lazy
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123456");
return new User("mx", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication.getPrincipal();
}
}
资源服务
@Configuration
@EnableResourceServer
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.authorizeRequests()
.anyRequest().authenticated()
.and().requestMatchers().antMatchers("/user/**");
}
}
认证服务
@Configuration // 授权模式 简单模式 密码模式 客户端模式
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManagerBean;
// @Autowired
// private TokenStore redisTokenStore;
@Autowired
private UserService userService;
// 密码模式 刷新令牌
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// super.configure(endpoints);
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
// .tokenStore(redisTokenStore) //指定token存储到redis
.reuseRefreshTokens(false) //refresh_token是否重复使用
.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持GET,POST请求
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// super.configure(security);
//允许表单认证
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// super.configure(clients);
/**
* 授权码模式
* http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
*
* implicit: 简化模式
* http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
*
* password模式
* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
*
* 客户端模式
* http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
*
* 刷新令牌
* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
*/
clients.inMemory()
//配置client_id
.withClient("client")
//配置client-secret
.secret(passwordEncoder.encode("123123"))
//配置访问token的有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//配置申请的权限范围
.scopes("all")
//配置grant_type,表示授权类型
/**
* 配置grant_type,表示授权类型
* authorization_code: 授权码模式
* implicit: 简化模式
* password: 密码模式
* client_credentials: 客户端模式
* refresh_token: 更新令牌
*/
.authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token");
}
}
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
application.yml
spring:
redis:
host: 127.0.0.1
database: 0
config.java
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
use
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
.tokenStore(tokenStore) //指定token存储到redis
.reuseRefreshTokens(false) //refresh_token是否重复使用
.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
}
用户只需要登录一次就可以访问权限范围内的所有应用子系统
适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分
核心原理:
通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的 Cookie 来实现单点登录
核心原理:
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
application.yml
server:
port: 8080
spring:
application:
name: oauth2-jdbc-demo
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql.localhost.com:3306/oauth2-test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// String password = passwordEncoder().encode("123456");
// auth.inMemoryAuthentication()
// .withUser("admin").password(password).roles("ADMIN")
// .and()
// .withUser("mx").password(password).roles("USER");
auth.userDetailsService(userDetailsService());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/check_token"); // 将 check_token 暴露出去,否则资源服务器访问时报 403 错误
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return new UserServiceImpl();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
AuthorizationServerConfig.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManagerBean;
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
@Bean
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(dataSource);//读取oauth_client_details表
}
// @Autowired
// private PasswordEncoder passwordEncoder;
/**
* 授权码模式
* http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
*
* implicit: 简化模式
* http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
*
* password模式
* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
*
* 客户端模式
* http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
*
* 刷新令牌
* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.inMemory()
// .withClient("client")
// .secret(passwordEncoder.encode("123123"))
// .authorizedGrantTypes("authorization_code")
// .scopes("app")
// .redirectUris("http://www.baidu.com");
clients.withClientDetails(jdbcClientDetailsService());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
.tokenStore(tokenStore()) //指定token存储到redis
.reuseRefreshTokens(false) //refresh_token是否重复使用
.userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单认证
security.allowFormAuthenticationForClients()
// 配置校验token需要带入clientId 和clientSeret配置
.checkTokenAccess("isAuthenticated()");
}
}
application.yml
spring:
application:
name: oauth2-resource-demo
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql.localhost.com:3306/oauth2-test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
security:
oauth2:
client:
client-id: client
client-secret: 123123
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
token-info-uri: http://localhost:8080/oauth/check_token
id: ${spring.application.name}
server:
port: 8088
ResourceServerConfig.java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${spring.application.name}")
private String appName;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/contents/").hasAuthority("SystemContent")
.antMatchers("/contents/view/**").hasAuthority("SystemContentView")
.antMatchers("/contents/insert/**").hasAuthority("SystemContentInsert")
.antMatchers("/contents/update/**").hasAuthority("SystemContentUpdate")
.antMatchers("/contents/delete/**").hasAuthority("SystemContentDelete");
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(appName);
super.configure(resources);
}
}
server:
port: 8082
servlet:
session:
cookie:
name: OAUTH2-SSO-CLIENT-DEMO-SESSION-${server.port} #防止Cookie冲突,冲突会导致登录验证不通过
security: #与授权服务器对应的配置
oauth2:
client:
client-id: client
client-secret: 123123
user-authorization-uri: http://localhost:8080/oauth/authorize
access-token-uri: http://localhost:8080/oauth/token
resource:
token-info-uri: http://localhost:8080/oauth/check_token
@EnableOAuth2Sso
@SpringBootApplication
@EnableOAuth2Sso
public class OAuth2ClientDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2ClientDemoApplication.class, args);
}
}
网关在认证授权体系里主要负责两件事
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
Gateway AuthenticationFilter 认证过滤器
@Component
@Order(0)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
@Autowired
private RestTemplate restTemplate;
private static Set<String> shouldSkipUrl = new LinkedHashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// 不拦截认证的请求
shouldSkipUrl.add("/oauth/token");
shouldSkipUrl.add("/oauth/check_token");
shouldSkipUrl.add("/user/getCurrentUser");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getURI().getPath();
//不需要认证的url
if(shouldSkip(requestPath)) {
return chain.filter(exchange);
}
//获取请求头
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
//请求头为空
if(StringUtils.isEmpty(authHeader)) {
throw new RuntimeException("请求头为空");
}
TokenInfo tokenInfo=null;
try {
//获取token信息
tokenInfo = getTokenInfo(authHeader);
}catch (Exception e) {
throw new RuntimeException("校验令牌异常");
}
// tokenInfo
exchange.getAttributes().put("tokenInfo",tokenInfo);
return chain.filter(exchange);
}
private boolean shouldSkip(String reqPath) {
for(String skipPath:shouldSkipUrl) {
if(reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
private TokenInfo getTokenInfo(String authHeader) {
// 往授权服务发请求 /oauth/check_token
// 获取token的值
String token = StringUtils.substringAfter(authHeader, "bearer ");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//必须 basicAuth clienId clientSecret
headers.setBasicAuth(MDA.clientId, MDA.clientSecret);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", token);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
ResponseEntity<TokenInfo> response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class);
return response.getBody();
}
}
Gateway AuthorizationFilter 鉴权过滤器
@Component
@Order(1)
public class AuthorizationFilter implements GlobalFilter, InitializingBean {
private static Set<String> shouldSkipUrl = new LinkedHashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// 不拦截认证的请求
shouldSkipUrl.add("/oauth/token");
shouldSkipUrl.add("/oauth/check_token");
shouldSkipUrl.add("/user/getCurrentUser");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getURI().getPath();
//不需要认证的url
if(shouldSkip(requestPath)) {
return chain.filter(exchange);
}
TokenInfo tokenInfo = exchange.getAttribute("tokenInfo");
if(!tokenInfo.isActive()) {
throw new RuntimeException("token过期");
}
hasPremisson(tokenInfo,requestPath);
return chain.filter(exchange);
}
private boolean shouldSkip(String reqPath) {
for(String skipPath:shouldSkipUrl) {
if(reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
private boolean hasPremisson(TokenInfo tokenInfo,String currentUrl) {
boolean hasPremisson = false;
//登录用户的权限集合判断
List<String> premessionList = Arrays.asList(tokenInfo.getAuthorities());
for (String url: premessionList) {
if(currentUrl.contains(url)) {
hasPremisson = true;
break;
}
}
if(!hasPremisson){
throw new RuntimeException("没有权限");
}
return hasPremisson;
}
}
辅助类
/**
* 常量类
*/
public class MDA {
public static final String clientId = "gateway-server";
public static final String clientSecret = "123123";
public static final String checkTokenUrl = "http://oauth2-jdbc-demo/oauth/check_token";
}
@Data
public class TokenInfo {
private boolean active;
private String client_id;
private String[] scope;
private String username;
private String[] aud;
private Date exp;
private String[] authorities;
}
@Configuration
public class RibbonConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
OAuth 2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
JWT令牌的缺点:
一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)
头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。
这也可以被表示成一个JSON对象:
{
"alg": "HS256",
"typ": "JWT"
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
然后将其进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'mendd'); // EGSi4DskFrnG-61ydOuB1z5F9ABtJZrfHRFvxVjppkc
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EGSi4DskFrnG-61ydOuB1z5F9ABtJZrfHRFvxVjppkc
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
如何应用
一般是在请求头里加入Authorization,并加上Bearer标注:
{
headers: {
'Authorization': 'Bearer ' + token
}
}
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
public class JwtDemoTest {
private static final String SECRET_KEY = "123123";
public static String testToken() {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("888") //声明的标识{"jti":"888"}
.setSubject("MenDD") //主体,用户{"sub":"Mendd"}
.setIssuedAt(new Date()) //创建日期{"ita":"xxxxxx"}
.setExpiration(new Date(System.currentTimeMillis()+60*1000)) //设置过期时间 1分钟
// .addClaims(map) //直接传入map
.claim("roles", "admin")
.claim("logo", "mendd.jpg")
.signWith(SignatureAlgorithm.HS256, SECRET_KEY);//签名手段,参数1:算法,参数2:盐
String token = jwtBuilder.compact();
System.out.println("token: " + token);
System.out.println("======parse======");
String[] split = token.split("\\.");
System.out.println("header: " + Base64Codec.BASE64.decodeToString(split[0]));
System.out.println("payload: " + Base64Codec.BASE64.decodeToString(split[1]));
//无法解密
System.out.println("signature: " + Base64Codec.BASE64.decodeToString(split[2]));
return token;
}
public static void testParseToken(String token){
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("issuedAt:"+claims.getIssuedAt());
DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sf.format(claims.getExpiration()));
System.out.println("当前时间:"+sf.format(new Date()));
System.out.println("roles:"+claims.get("roles"));
System.out.println("logo:"+claims.get("logo"));
}
public static void main(String[] args) {
String token = testToken();
System.out.println(token);
String extToken = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNZW5ERCIsImlhdCI6MTY2ODc1MzM2MywiZXhwIjoxNjY4NzUzNDIzLCJyb2xlcyI6ImFkbWluIiwibG9nbyI6Im1lbmRkLmpwZyJ9.F5o7yPI64ZSAI7OVp5BNPSu62u8aHaQmfcBdLhlpONQ";
testParseToken(token);
testParseToken(extToken);
}
}
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-jwtartifactId>
<version>1.0.9.RELEASEversion>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter accessTokenConverter = new
JwtAccessTokenConverter();
//配置JWT使用的秘钥
accessTokenConverter.setSigningKey("123123");
return accessTokenConverter;
}
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
}
在授权服务器配置中指定令牌的存储策略为JWT
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManagerBean;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置JWT的内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
.tokenStore(tokenStore) //配置存储令牌策略
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain) //配置tokenEnhancer
.reuseRefreshTokens(false) //refresh_token是否重复使用
.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单认证
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/**
*授权码模式
*http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
*http://localhost:8080/oauth/authorize?response_type=code&client_id=client
*
* password模式
* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
*
*
*
* 刷新令牌
* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
*/
clients.inMemory()
//配置client_id
.withClient("client")
//配置client-secret
.secret(passwordEncoder.encode("123123"))
//配置访问token的有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//配置申请的权限范围
.scopes("all")
/**
* 配置grant_type,表示授权类型
* authorization_code: 授权码
* password: 密码
* client_credentials: 客户端
* refresh_token: 更新令牌
*/
.authorizedGrantTypes("authorization_code","password","refresh_token");
}
}
JWT内容增强器
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("enhance", "enhance info");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
使用jjwt工具类来解析Authorization头中存储的JWT内容
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication,
HttpServletRequest request) {
String header = request.getHeader("Authorization");
String token = null;
if(header!=null){
token = header.substring(header.indexOf("bearer") + 7);
}else {
token = request.getParameter("access_token");
}
return Jwts.parser()
.setSigningKey("123123".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}
}
http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
{
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg",
token_type: "bearer",
refresh_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlNmM0OWIyYS0zZjgwLTRkNjMtYTU4Ny00NzQ1MWYwMzAxMmIiLCJleHAiOjE2Njk2MTkzNTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjFhNDc3NTc2LTAwOTItNDYwYy1hM2RlLWIyYzk3ODAyMzMxZCIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.Yk8J4tf49WrfRbd6yZrc8WqyNIL98XTygiI9tzVhxCA",
expires_in: 3599,
scope: "all",
appname: "enhance info appname mendd",
enhance: "enhance info",
jti: "e6c49b2a-3f80-4d63-a587-47451f03012b",
}
curl --location --request GET 'http://localhost:8080/user/getCurrentUser' \
--header 'Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg'
http://localhost:8080/user/getCurrentUser?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg
{
appname: "enhance info appname mendd",
user_name: "mx",
scope: [
"all"
],
exp: 1668758955,
authorities: [
"admin"
],
jti: "e6c49b2a-3f80-4d63-a587-47451f03012b",
client_id: "client",
enhance: "enhance info",
}
EnableAuthorizationServer
//token转换
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//jwt的秘钥 对称加密
//converter.setSigningKey("123456");
//jwt的密钥 非对称加密
converter.setKeyPair(keyPair());
return converter;
}
//jwt的密钥 非对称加密
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource(jwtCAProperties.getKeyPairName()),
jwtCAProperties.getKeyPairSecret().toCharArray());
return keyStoreKeyFactory.getKeyPair(
jwtCAProperties.getKeyPairAlias(),
jwtCAProperties.getKeyPairStoreSecret().toCharArray());
}
//token 增强
@Bean
public TokenEnhancer demoTokenEnhancer() {
return new DemoTokenEnhancer();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(demoTokenEnhancer(),jwtAccessTokenConverter()));
endpoints.tokenStore(tokenStore()) //授权服务器颁发的token 怎么存储的
.tokenEnhancer(tokenEnhancerChain) //token 增强
.userDetailsService(demoUserDetailService) //用户来获取token的时候需要 进行账号密码
.authenticationManager(authenticationManager);
}
jwt token 增强 (可以加入有效期、自己的字段)
public class DemoTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
final Map<String, Object> retMap = new HashMap<>();
//这里暴露memberId到Jwt的令牌中,后期可以根据自己的业务需要 进行添加字段
additionalInfo.put("memberId",memberDetails.getUmsMember().getId());
additionalInfo.put("nickName",memberDetails.getUmsMember().getNickname());
additionalInfo.put("integration",memberDetails.getUmsMember().getIntegration());
retMap.put("additionalInfo",additionalInfo);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap);
return accessToken;
}
}
JwtCAProperties.java
@Data
@ConfigurationProperties(prefix = "demo.jwt")
public class JwtCAProperties {
/**
* 证书名称
*/
private String keyPairName;
/**
* 证书别名
*/
private String keyPairAlias;
/**
* 证书私钥
*/
private String keyPairSecret;
/**
* 证书存储密钥
*/
private String keyPairStoreSecret;
}
application.yml
demo:
jwt:
keyPairName: jwt.jks #证书名称
keyPairAlias: jwt #证书别名
keyPairSecret: 123456 #证书私钥
keyPairStoreSecret: 123456 #证书存储密钥
jks
证书文件keytool ‐genkey pair ‐alias jwt ‐keyalg RSA ‐keysize 2048 ‐keystore D:/jwt/jwt.jks
命令格式
keytool
-genkeypair 生成密钥对
-alias jwt(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法) -keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore D:/jwt/jwt.jks(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
-storetype (指定密钥仓库类型)
使用 "keytool -help" 获取所有可用命令
将生成的jwt.jks文件cope到授权服务器的resource目录下
查看公钥信息
执行:keytool ‐list ‐rfc ‐‐keystore jwt.jks | openssl x509 ‐inform pem ‐pubkey
http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
curl --location --request POST 'http://localhost:9999/oauth/token' \
> --form 'redirect_uri="http://www.baidu.com"' \
> --form 'code="1s1O2H"' \
> --form 'grant_type="authorization_code"' \
> --form 'client_id="client"' \
> --form 'client_secret="123123"'
response
{
"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0ZXN0Iiwic2NvcGUiOlsiYWxsIl0sImFkZGl0aW9uYWxJbmZvIjp7Im5pY2tOYW1lIjoidGVzdCIsImludGVncmF0aW9uIjpudWxsLCJtZW1iZXJJZCI6MX0sImV4cCI6MTY3MDYzNjc4OCwiYXV0aG9yaXRpZXMiOlsiVEVTVCJdLCJqdGkiOiJiYjk3OWFlNC0zYTI5LTRjMzUtOGNiOC0yZTg4MmYzNGIyNGYiLCJjbGllbnRfaWQiOiJjbGllbnQifQ.BShdjIUYBnMXum57QU-AGeTUbJi2TjurADZEEL4-On-t8i_s3n-g0ARyu88cwsFyBdOem2OT8WLuF_Dauu-H6vKeo0mqoYvc21x0rLBi9qChbOkyWEL1Ndef96z_fnzHkIceDlKgIwlOxeiZuNSrzeRihsc-7HnbMnd_iZMV3LlP5W40_ogCsQ29BoD1jK9RV97XKa5AzYAMkfgoaUR2-4NVzYM13EEZZ5Mt2qSfb-RSR-6RSDNiPcYMksuh1bPX2Wrc6IoygbJ4gT7wHW-GOQBIQKd4PLve33OJB4UYF7IDhuVGm5ir-6canUSvJWMBdrEhof-45gAm9pd_QuKkxA",
"token_type":"bearer",
"refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0ZXN0Iiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJiOTc5YWU0LTNhMjktNGMzNS04Y2I4LTJlODgyZjM0YjI0ZiIsImFkZGl0aW9uYWxJbmZvIjp7Im5pY2tOYW1lIjoidGVzdCIsImludGVncmF0aW9uIjpudWxsLCJtZW1iZXJJZCI6MX0sImV4cCI6MTY3MzE4NTU4OCwiYXV0aG9yaXRpZXMiOlsiVEVTVCJdLCJqdGkiOiJhZjYxOWRjYS0wOWUxLTRmMzgtOGNmOC04YzEwMzA0MzI2MTciLCJjbGllbnRfaWQiOiJjbGllbnQifQ.jSHdkZ-iEFXG_DZoRHCQWLd3xYQaXwx6O25TMJSj996duMZNxM3FG0sCaRH8PBCfFCKrdfqTOERLQ09qjFsQPGcvc69eAfd1aM3pf1VxX8S2Ckvdlm6-FCHo5V8AQKKkOyaBfLgFlN-jvJkQVhk3aWxTtSCeZabqTCIS-MXSXoMWHRfYH265bSvjAh2uvjilnxp9gHaWkmkD1cfvKgMXz4wRZ45qDsPtCVtOc539uxpK4ScjT_rtqFQvFQdu3yjpLWW7wqTE3CX_1tVJHGPZ45kzxHWNv0H6gV61-fmZqry0DzgUExIv8BLItED2m_R-gijmAJq_yovDch9m5CwWgw",
"expires_in":43199,
"scope":"all",
"additionalInfo":{
"nickName":"test",
"integration":null,
"memberId":1
},
"jti":"bb979ae4-3a29-4c35-8cb8-2e882f34b24f"
}
通过获取公钥: keytool -list -rfc -keystore jwt.jks | openssl x509 -inform pem -pubkey
通过接口获取公钥
curl --location --request GET 'http://localhost:9999/oauth/token_key' \
--header 'Authorization: Basic Y2xpZW50OjEyMzEyMw=='
@Component
@Slf4j
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthorizationFilter implements GlobalFilter,Ordered,InitializingBean {
@Autowired
private RestTemplate restTemplate;
/**
* 请求各个微服务 不需要用户认证的URL
*/
@Autowired
private NotAuthUrlProperties notAuthUrlProperties;
/**
* jwt的公钥,需要网关启动,远程调用认证中心去获取公钥
*/
private PublicKey publicKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String currentUrl = exchange.getRequest().getURI().getPath();
//1:不需要认证的url
if(shouldSkip(currentUrl)) {
//log.info("跳过认证的URL:{}",currentUrl);
return chain.filter(exchange);
}
//log.info("需要认证的URL:{}",currentUrl);
//第一步:解析出我们Authorization的请求头 value为: “bearer XXXXXXXXXXXXXX”
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
//第二步:判断Authorization的请求头是否为空
if(StringUtils.isEmpty(authHeader)) {
log.warn("需要认证的url,请求头为空");
throw new GateWayException(ResultCode.AUTHORIZATION_HEADER_IS_EMPTY);
}
//第三步 校验我们的jwt 若jwt不对或者超时都会抛出异常
Claims claims = JwtUtils.validateJwtToken(authHeader,publicKey);
//第四步 把从jwt中解析出来的 用户登陆信息存储到请求头中
ServerWebExchange webExchange = wrapHeader(exchange,claims);
return chain.filter(webExchange);
}
/**
* 方法实现说明:把我们从jwt解析出来的用户信息存储到请求中
*/
private ServerWebExchange wrapHeader(ServerWebExchange serverWebExchange,Claims claims) {
String loginUserInfo = JSON.toJSONString(claims);
//log.info("jwt的用户信息:{}",loginUserInfo);
String memberId = claims.get("additionalInfo",Map.class).get("memberId").toString();
String nickName = claims.get("additionalInfo",Map.class).get("nickName").toString();
//向headers中放文件,记得build
ServerHttpRequest request = serverWebExchange.getRequest().mutate()
.header("username",claims.get("user_name",String.class))
.header("memberId",memberId)
.header("nickName",nickName)
.build();
//将现在的request 变成 change对象
return serverWebExchange.mutate().request(request).build();
}
/* private boolean hasPremisson(Claims claims,String currentUrl) {
boolean hasPremisson = false;
//登陆用户的权限集合判断
List premessionList = claims.get("authorities",List.class);
for (String url: premessionList) {
if(currentUrl.contains(url)) {
hasPremisson = true;
break;
}
}
if(!hasPremisson){
log.warn("权限不足");
throw new GateWayException(SystemErrorType.FORBIDDEN);
}
return hasPremisson;
}*/
/**
* 方法实现说明:不需要授权的路径
*/
private boolean shouldSkip(String currentUrl) {
//路径匹配器(简介SpringMvc拦截器的匹配器)
//比如/oauth/** 可以匹配/oauth/token /oauth/check_token等
PathMatcher pathMatcher = new AntPathMatcher();
for(String skipPath:notAuthUrlProperties.getShouldSkipUrls()) {
if(pathMatcher.match(skipPath,currentUrl)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
/**
* 方法实现说明:网关服务启动 生成公钥
*/
@Override
public void afterPropertiesSet() throws Exception {
//初始化公钥 http://demo-authcenter/oauth/token_key
this.publicKey = JwtUtils.genPulicKey(restTemplate);
}
}
从Token JWT中验证并获取信息 JwtUtils.validateJwtToken(authHeader,publicKey);
public static Claims validateJwtToken(String authHeader, PublicKey publicKey) {
String token =null ;
try{
token = StringUtils.substringAfter(authHeader, AUTH_HEADER);
Jwt<JwsHeader, Claims> parseClaimsJwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims claims = parseClaimsJwt.getBody();
return claims;
}catch(Exception e){
log.error("校验token异常:{},异常信息:{}",token,e.getMessage());
throw new GateWayException(ResultCode.JWT_TOKEN_EXPIRE);
}
}
restTemplate 是微服务的名字 JwtUtils.genPulicKey(restTemplate)
ribbon 是在springboot启动之后加载的, 扩展的是 SmartInitializingSingleton , 而这里后期publicKey是在InitializingBean中获取的
RibbonConfig
@Configuration
public class RibbonConfig {
/**
* 方法实现说明:原生的RestTemplate +@LB不行 因为在
* InitializingBean方法执行前我们的RestTemplate还没有被增强
* 需要自己改写RestTemplate
*/
// @Bean
// public DemoRestTemplate restTemplate(DiscoveryClient discoveryClient) {
// return new DemoRestTemplate(discoveryClient);
// }
/**
*
* 手动注入loadBalancerInterceptor拦截器,实现负载均衡功能
* @param loadBalancerInterceptor
* @return
*
*/
@Bean
public RestTemplate restTemplate(LoadBalancerInterceptor loadBalancerInterceptor){
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> list = new ArrayList();
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
return restTemplate;
}
}
DemoRestTemplate
/**
* 根据RestTemplate特性自己改造
*/
@Slf4j
public class DemoRestTemplate extends RestTemplate {
private DiscoveryClient discoveryClient;
public DemoRestTemplate(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
//判断url的拦截路径,然后去redis(作为注册中心)获取地址随机选取一个
log.info("请求的url路径为:{}",url);
url = replaceUrl(url);
log.info("替换后的路径:{}",url);
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
} finally {
if (response != null) {
response.close();
}
}
}
/**
* 把服务实例名称替换为ip:端口
*/
private URI replaceUrl(URI url){
//解析我们的微服务的名称
String sourceUrl = url.toString();
String [] httpUrl = sourceUrl.split("//");
int index = httpUrl[1].replaceFirst("/","@").indexOf("@");
String serviceName = httpUrl[1].substring(0,index);
//通过微服务的名称去nacos服务端获取 对应的实例列表
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(serviceName);
if(serviceInstanceList.isEmpty()) {
throw new RuntimeException("没有可用的微服务实例列表:"+serviceName);
}
//采取随机的获取一个
Random random = new Random();
Integer randomIndex = random.nextInt(serviceInstanceList.size());
log.info("随机下标:{}",randomIndex);
String serviceIp = serviceInstanceList.get(randomIndex).getUri().toString();
log.info("随机选举的服务IP:{}",serviceIp);
String targetSource = httpUrl[1].replace(serviceName,serviceIp);
try {
return new URI(targetSource);
} catch (URISyntaxException e) {
e.printStackTrace();
}
return url;
}
}