前面我们已经介绍过了分布式项目的几个基本模块,现在介绍一下服务之间的认证。这一章主要用到的工具有Oauth2和JWT令牌。
Oauth是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。Oauth2.0是Oauth协议的延续版本,但不想后兼容Oauth1.0就是完全废止了Oauth1.0。
Oauth几个重要的角色:
(1)客户端:就是指前端服务
(2)资源拥有者:就是用户
(3)认证服务器:通俗点说就是我们在后面写的验证用户是否合法的服务(例如我们把shiro单独出来做一个服务)
(4)资源服务:可以查询数据的服务
跟普通服务来说,oauth不光是输入用户名和密码就能获取信任。服务方会给客户端一个身份标识,只有携带这个标识才可以访问服务
client_id:客户端标识 client_secret:客户端密匙
JWT令牌:JSON Web Token是一个开放的行业标准,它定义了一种简介的,自包含的协议格式,用户在通信双方专递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公匙/私匙对来签名,防止被篡改。
JWT优点:
- jwt基于json,非常方便解析
- 可以在令牌中自定义丰富的内容,易扩展
- 通过非对称算法及数字签名技术,防止篡改,安全性高
- 资源服务使用jwt可不依赖认证服务即可完成授权
缺点:
Jwt令牌较长,占存储空间比较大
Jwt令牌分为三个部分:
- header
- payload(负载,内容是一个json对象)
- Signature(签名)
分布式系统认证技术方案
流程:
- 客户端请求认证服务
- 认证服务验证用户,通过则返回token
- 客户端携带token访问资源
- 网关拦截,判断token是否有效,确定有效解析token,获取用户身份,转发请求
- 资源服务获取用户身份,校验权限,权限通过,则访问
代码部分
我们还是按照之前的服务走,改造一下,不在重头建立了。
1、 改造父工程
父工程没有大的改变,我在们pom上加几个用到的依赖,主要作用是控制版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
2、创建cloud-commons模块
这个模块主要是存放工具类和实体类,就是其他模块都可能用到的东西,创造模块过程跟以前一样。
创建实体类CUser(这里我没用User因为后面有个配置会跟user重名,起名CUser做区分)
public class CUser {
private Integer sid;
private String userName;
private String password;
private String email;
public Integer getSid() {
return sid;
}
public void setSid(Integer sid) {
this.sid = sid;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
把这个模块用maven工具install一下。这样我们就可以把这个服务引入到其他模块了,坐标为
<dependency>
<groupId>com.study</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
后面用到的服务我们会直接引用,不在特别说了。
3、 创建认证服务(oauth9000)
准备环境,在这个服务中我们需要用到数据库,所以我们先创建一下我们会用到的表
CREATE TABLE `s_user` (
`sid` int(11) NOT NULL,
`userName` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(50) DEFAULT NULL,
PRIMARY KEY (`sid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `s_user` (`sid`, `userName`, `password`, `email`) VALUES ('1', 'admin', '$2a$10$KCJaXoAJk3uXYyHtF47XHOYOTVkJo5mWBO0d4/JPypYvzilWsayDi', '123456.com');
CREATE TABLE `s_permission` (
`sid` int(11) NOT NULL,
`permission` varchar(50) NOT NULL,
`uid` int(11) NOT NULL,
PRIMARY KEY (`sid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `s_permission` (`sid`, `permission`, `uid`) VALUES ('1', 'p1', '1');
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) 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(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
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 ('c1', 'res1', '$2a$10$vhFNc2v9Lesl7GCa1yuBN.0IFPNNymu2R/ybRTejna8OcZmGCzM5i', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, '7200', '259200', NULL, 'false');
CREATE TABLE `oauth_code` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) DEFAULT NULL,
`authentication` blob,
KEY `code_index` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
模块基本结构图
具体路径我就不再说了。
Pom文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloudstudy</artifactId>
<groupId>com.study</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>oauth9000</artifactId>
<dependencies>
<!-- SpringCloud alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--sentinel持久化-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.study</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml(注意:这里我们用到了数据库,关于一些路径按照自己的修改)
server:
port: 9000
servlet:
context-path: /security
mybatis:
type-aliases-package: com.study
mapperLocations: classpath:mapper/*.xml
spring:
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/advance?useSSL=true&serverTimezone=GMT
username: root
password: root
application:
name: nacos-oauth
cloud: #注册到nacos
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
datasource:
ds1:
nacos:
server-addr: localhost:8848 #nacos地址
dataID: nacos-payment-provider
groupId: DEFAULT_GROUP
data-type: json #注意是json类型
rule-type: flow
management:
endpoints:
web:
exposure:
include: "*"
创建启动类:OauthMain9000
@SpringBootApplication
@EnableDiscoveryClient
public class OauthMain9000 {
public static void main(String[] args) {
SpringApplication.run(OauthMain9000.class,args);
}
}
在resources创建mapper文件夹,在文件夹里面创建UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace用于绑定Dao接口 -->
<mapper namespace="com.study.mapper.UserMapper">
<select id="getUserByUserName" parameterType="String" resultType="CUser">
select
sid,
userName,
password,
email
from s_user where userName = #{userName}
</select>
<select id="getPermissionByUid" parameterType="Integer" resultType="String">
select
permission
from s_permission where uid = #{uid}
</select>
</mapper>
创建UserMapper
@Mapper
public interface UserMapper {
public CUser getUserByUserName(@Param("userName") String userName);
public List<String> getPermissionByUid(@Param("uid")Integer uid);
}
创建MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
CUser CUser = userMapper.getUserByUserName(userName);
if(CUser ==null){
return null;
}
List<String> permission = userMapper.getPermissionByUid(CUser.getSid());
//将permissions转成数组
String[] permissionArray = new String[permission.size()];
permission.toArray(permissionArray);
UserDetails userDetails = User.withUsername(JSON.toJSONString(CUser)).password(CUser.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
创建TokenConfig,生成jwt格式的令牌
/**
* 持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。
* 并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,
* 如其命名,所有的 令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现
*/
@Configuration
public class TokenConfig {
private String SINGING_KEY = "uaa123";
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SINGING_KEY);
return converter;
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConverter());
}
}
配置授权服务器AuthorizationServer
/**
* 配置授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
PasswordEncoder passwordEncoder;
//将客户端信息存储到数据库
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
//令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
/**
* 用来配置客户端详情服务,客户端详情在这里初始化
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
// clients.inMemory()//暂时使用inmemory部署
// .withClient("c1")//clientId:(必须的)用来标识客户的Id。
// .secret(new BCryptPasswordEncoder().encode("secret"))//客户端密钥 //客户端安全码(需要加密)
// .resourceIds("res1")//资源
// .authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")//该client允许的授权类型
// .scopes("all")//用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围
// .autoApprove(false)//
// .redirectUris("http://www.baidu.com");//加上验证回调地址
}
/**
* 用来配置令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开
.checkTokenAccess("permitAll()") //checkToken这个endpoint完全公开
.allowFormAuthenticationForClients(); //允许表单认证
}
/**
* 用来配置令牌的访问端点和令牌服务
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//设置授权码模式的授权码如何存取,暂时采用内存方式
// @Bean
// public AuthorizationCodeServices authorizationCodeServices() {
// return new InMemoryAuthorizationCodeServices();
// }
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取
}
}
这里我简单说一下,我们把客户端详情配置到是数据库中了。oauth_client_details这张表就是客户端详情
Client_id:客户端标识,
Resource_ids:资源
Client_secret:客户端密匙(数据库中存的是加密之后的,代码中我们配置的加密类是PasswordEncoder,客户端传的时候用明文)
Scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes:此客户端可以使用的授权类型,默认为空
authorities:此客户端可以使用的权限(基于Spring Security authorities)。
web_server_redirect_uri:回调地址,授权码模式会用到
创建WebSecurityConfig(web安全配置)
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//认证管理器
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编辑器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").permitAll() //配置无需登录资源
// .anyRequest().authenticated() //其他资源允许
.and()
.formLogin();//表单登录
}
}
现在我们来启动这个服务来检测一下。首先却要启动nacos和sentinel,然后启动oauth9000服务
现在我们说一下几个认证模式:
授权码模式:
首先访问
http://localhost:9000/security/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
其中security是我们在application配置的路径
跳转到登录页面,我们在数据库有一个admin用户,密码也是用PasswordEncoder加密的,明文是123456,输入登录:出现下面页面
选择Approve点击授权,会跳到百度页面,为什么跳到百度页面,是因为我们配置的跳转地址是http://www.baidu.com正常开发中我们需要自己编写一个页面实现跳转,我们看地址栏中
把这个code复制一下,打开postman
其中client_secret是明文,数据库中存的是密文,grant_type输入authorization_code即授权码模式,把刚才复制code输入进去,点击send
便可获得access_token,现在我们来校验一下token是否可用
把刚才得到的token复制进去,就可以获得自己的信息,证明token有效。
现在再来说一下密码模式:
直接用postman把grant_type修改为password,校验同上
其他几个模式不在一个一个展示了,有兴趣的话,可以自己去了解一下。
4、改造网关服务
首先在pom中加入新依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--自己创建的工具模块-->
<dependency>
<groupId>com.study</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
修改application.yml文件
server:
port: 8000
spring:
application:
name: nacos-getway #本服务的名称
cloud:
gateway: #注册网关
discovery:
locator:
enabled: true #开启注册中心路由功能
lower-case-service-id: true
routes:
- id: payment_routh
# uri: http://localhost:8001
uri: lb://nacos-payment-provider #此处如果有问题,请注意依赖spring-cloud-starter-netflix-eureka-client依赖不能错
predicates:
- Path=/payment/**
# - id: payment_routh2
# # uri: http://localhost:8001
# uri: lb://nacos-payment-provider
# predicates:
# - Path=/payment/lb/** #指定路径
- id: oauth_routh
uri: lb://nacos-oauth
predicates:
- Path=/security/** #指定路径
# - After=2020-06-17T11:09:08.176+08:00[Asia/Shanghai] #在这个时间之后才能访问
# - Cookie=username,zs #带指定cookie再能访问
# - Header=X-Request-Id, \d+ #携带指定请求头才能访问 前面属性名称 后面正则表达式(整数)
# - Host=**.baidu.com #接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.做分隔符
# - Method=GET #指定请求方法
# - Query=username, \d+ #指定参数,可接受两个值一个属性名,后面是正则表达式
nacos: #注册到nacos
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
datasource:
ds1:
nacos:
server-addr: localhost:8848 #nacos地址
dataID: nacos-payment-provider
groupId: DEFAULT_GROUP
data-type: json #注意是json类型
rule-type: flow
management:
endpoints:
web:
exposure:
include: "*"
创建config包:
在下面创建:TokenConfig类
@Configuration
public class TokenConfig {
private String SIGNING_KEY="uaa123";
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
tokenConverter.setSigningKey(SIGNING_KEY);
return tokenConverter;
}
}
创建SecurityConfig类
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}
创建filter包
创建GlobleLogFilter
我们在这个类里面校验token,如果是有效token,我们解析token获得用户身份信息和权限,把用户信息加密放到header中,网关转发请求到资源服务,就可以在请求头中获取用户信息和权限。
@Component
public class GlobleLogFilter implements GlobalFilter, Ordered {
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//1 授权服务所有放行
if (pathMatcher.match("/security/**", requestUrl)) {
return chain.filter(exchange);
}
//2 检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(token)) {
return noTokenMono(exchange);
}
//3 判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();
//取出用户身份信息
String user_name = additionalInformation.get("user_name").toString();
CUser CUser = JSON.parseObject(user_name, CUser.class);
//获取用户权限
List<String> authorities = (List<String>) additionalInformation.get("authorities");
JSONObject jsonObject=new JSONObject();
jsonObject.put("principal", CUser);
jsonObject.put("authorities",authorities);
//给header里面添加值
// String base64 = EncryptUtil.encodeUTF8StringBase64(jsonObject.toJSONString());
String base64 = Base64.encode(jsonObject.toJSONString());
ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("user-token", base64).build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return chain.filter(build);
} catch (InvalidTokenException e) {
return invalidTokenMono(exchange);
}
}
@Override
public int getOrder() {
return 0;
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
/**
* 无效的token
*/
private Mono<Void> invalidTokenMono(ServerWebExchange exchange) {
JSONObject json = new JSONObject();
json.put("status", HttpStatus.UNAUTHORIZED.value());
json.put("data", "无效的token");
return buildReturnMono(json, exchange);
}
private Mono<Void> buildReturnMono(JSONObject json, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
byte[] bits = json.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
private Mono<Void> noTokenMono(ServerWebExchange exchange) {
JSONObject json = new JSONObject();
json.put("status", HttpStatus.UNAUTHORIZED.value());
json.put("data", "没有token");
return buildReturnMono(json, exchange);
}
}
5、改造payment9001(资源服务)
首先我们还是pom加入新依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--自己创建的工具模块-->
<dependency>
<groupId>com.study</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
创建config包
创建TokenConfig类
@Configuration
public class TokenConfig {
private static final String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
创建ResouceServerConfig类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.access("#oauth2.hasScope('ROLE_ADMIN')")//含有ROLE_ADMIN才能访问
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
创建filter包
创建AuthenticationFilter类,解析请求头把用户信息放到全文中,方便获取。
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("user-token");
if (StringUtils.isNotBlank(token)){
String json = Base64.decodeStr(token);
// String json = EncryptUtil.decodeUTF8StringBase64(token);
JSONObject jsonObject = JSON.parseObject(json);
//获取用户身份信息、权限信息
String principal = jsonObject.getString("principal");
CUser CUser = JSON.parseObject(principal, CUser.class);
JSONArray tempJsonArray = jsonObject.getJSONArray("authorities");
String[] authorities = tempJsonArray.toArray(new String[0]);
//身份信息、权限信息填充到用户身份token对象中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(CUser,null,
AuthorityUtils.createAuthorityList(authorities));
//创建details
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
}
修改PaymentController,添加一个方法
@GetMapping(value = "/payment/sayHellow")
public Map<String,Object> sayHellow(){
Map<String,Object> map = new HashMap<>();
//获取用户身份信息
CUser CUser = (CUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
map.put("data","Hellow!,I am from "+port);
map.put("user", CUser);
return map;
}
启动nacos和sentinel,启动三个服务,现在我们不通过9000端口获取token了,我们全部用网关转发请求
用的密码模式,获取token之后我们来访问资源服务
注意我们获得token之后会有一个"token_type": “bearer”,我们访问资源时,在headers中添加Authorization 值是bearer token 格式:bearer空格token
访问成功。