上面流程图描述了用户要操作的各个微服务,用户查看个人信息需要访问客户微服务,下单需要访问订单微服务,秒杀抢购商品需要访问秒杀微服务。每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色然后授权访问对应的功能。
用户访问的项目中,至少有3个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设计概念了。 所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图:
Java中有很多用户认证的框架都可以实现单点登录:
1、Apache Shiro.
2、CAS
3、Spring security
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的
接口协议。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认
证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程:
Oauth2.0认证流程如下: 引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
Oauth2包括以下角色:
1、客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购Android客户端、畅购Web客户端(浏览器端)、微信客户端等。
2、资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。
3、授权服务器(也称认证服务器) 用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。
4、资源服务器 存储资源的服务器,比如,畅购用户管理服务器存储了畅购的用户信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。
Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,项目中使用Oauth2可以实现实现如下功能:
1、本系统访问第三方系统的资源
2、外部系统访问本系统的资源
3、本系统前端(客户端) 访问本系统后端微服务的资源。
4、本系统微服务之间访问资源,例如:微服务A访问微服务B的资源,B访问A的资源。
本项目采用 Spring security + Oauth2+JWT完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了Oauth2协议,下图是项目认证架构图:
上面的是Oauth2(第三方登录的流程)这里是账号密码登录的流程
这里微服务网关是一个,这里只是因为流程的关系分开画了,然后在认证服务中会链接数据库查询一些用户基本信息放入token中,这样访问各个服务的时候用户信息就显示了
账号密码登录,网关对于登录和一些静态资源是放行的,可以访问。然后这里是验证登录和生成令牌的过程,这里采用jwt的方式经过RSA的私钥生成令牌,公钥去解析。
在认证服务的过程中会去链接数据库,访问两个数据库,一个是通过fegin的远程调用访问user,role,perssion表(都是多对多),这样返回的时候就会携带用户和权限信息,一个是oauth_client_details(配置数据库链接,自动访问)拿到第三方授权的账号密码(自关联)。有用户名密码查询正确,把uid和生成令牌的jti形成键值对放入cookie中,redis存放的是jti和对应的token信息,这样就能通过cookie中的uid找到jti,再找到redis中的token可以进行解析了
当用户访问服务的时候,会经过微服务网关,这里有两种配置解析的方式。
1.公钥存放在网关,判断cookie,redis中的指定值是否存在,最后通过公钥去解析令牌是否合法,如果都没问题就放行
2.如果私钥配置在微服务上的话,网关判断cookie和redis非空后,设置一下请求的响应头
request.mutate().header(“Authorization”,"Bearer "+jwt);
这就是令牌的传递,这样其他微服务就不需要再去cookie和redis去找令牌了,过来的时候就携带了
现在就分为认证服务和资源服务
登录授权后如果有改变权限或者增加微服务地址怎么动态的增加呢?
可以在后台维护一张字典表,专门用作配置,比如要令牌资源路径,或者订单状态,维护一对多的两张表,保存在DB中,取完保存在redis中,用Quartz定时组件来完成定时的更新。
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
1、JWT令牌较长,占存储空间比较大。
通过学习JWT令牌结构为自定义jwt令牌打好基础。
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一个例子如下:
下边是Header部分的内容
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
一个例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
一个例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
JWT令牌生成采用非对称加密算法
1、生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,changgou.jks保存了生成的证书
-storepass:密钥库的访问密码
查询证书信息:
keytool -list -keystore changgou.jks
2、导出公钥
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
安装资料目录下的Win64OpenSSL-1_1_1b.exe
配置openssl的path环境变量,
cmd进入changgou.jks文件所在目录执行如下命令:
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
public class CreateJwtTest {
/***
* 创建令牌测试
*/
@Test
public void testCreateToken(){
//证书文件路径
String key_location="changgou.jks";
//秘钥库密码
String key_password="changgou";
//秘钥密码
String keypwd = "changgou";
//秘钥别名
String alias = "changgou";
//访问证书路径
ClassPathResource resource = new ClassPathResource(key_location);
//创建秘钥工厂
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());
//读取秘钥对(公钥、私钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());
//获取私钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();
//定义Payload
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", "1");
tokenMap.put("name", "itheima");
tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
//生成Jwt令牌
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));
//取出令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。
在changgou-user-oauth创建测试类com.changgou.token.ParseJwtTest实现解析校验令牌数据,代码如下:
public class ParseJwtTest {
/***
* 校验令牌
*/
@Test
public void testParseToken(){
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取Jwt原始内容
String claims = jwt.getClaims();
System.out.println(claims);
//jwt令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',
`scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
导入1条初始化数据,其中加密字符明文为changgou:
INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$Yvkp3xzDcri6MAsPIqnzzeGBHez1QZR3A079XDdmNU4R725KrkXi2', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '43200', '43200', null, null);
Oauth2有以下授权模式:
1.授权码模式(Authorization Code)
2.隐式授权模式(Implicit)
3.密码模式(Resource Owner Password Credentials)
4.客户端模式(Client Credentials)
其中授权码模式和密码模式应用较多,本小节介绍授权码模式。
上边例举的黑马程序员网站使用QQ认证的过程就是授权码模式,流程如下:
1、客户端请求第三方授权
2、用户同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请 令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
请求认证服务获取授权码:
Get请求:
http://localhost:9200/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
首先跳转到登录页面:
输入账号和密码,点击Login。 Spring Security接收到请求会调用UserDetailsService接口的loadUserByUsername方法查询用户正确的密码。 当前导入的基础工程中客户端ID为changgou,秘钥也为changgou即可认证通过。
接下来进入授权页面:
点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=k45iLY就是返回的授权码, 每一个授权码只能使用一次
拿到授权码后,申请令牌。
Post请求:
http://localhost:9200/oauth/token
参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 http Basic认证。
什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子:
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
以上测试使用postman完成:
http basic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
Spring Security Oauth2提供校验令牌的端点,如下:
Get: http://localhost:9200/oauth/check_token?token= [access_token]
参数:
token:令牌
使用postman测试如下:
如果令牌校验失败,会出现如下结果:
如果令牌过期了,会如下如下结果:
刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。
测试如下: Post:http://localhost:9200/oauth/token
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。
测试如下:
Post请求:
http://localhost:9200/oauth/token
携带参数:
grant_type:密码模式授权填写password
username:账号
password:密码
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:
上图的业务流程如下:
1、客户端请求认证服务申请令牌
2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer令牌。
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息
基本上所有微服务都是资源服务,这里我们在课程管理服务上配置授权控制,当配置了授权控制后如要访问课程信 息则必须提供令牌。
1、配置公钥 ,将 changggou_user_auth 项目中public.key复制到changgou_service_user中
2、添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
3、配置每个系统的Http请求路径安全控制策略以及读取公钥信息识别令牌,如下:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
不携带令牌访问http://localhost:9005/user
由于该地址受访问限制,需要授权,所以出现如下错误:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
携带令牌访问http://localhost:9005/user
在http header中添加 Authorization: Bearer 令牌
当输入错误的令牌也无法正常访问资源。
1、用户登录,请求认证服务
2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie
3、用户访问资源页面,带着cookie到网关
4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token
使用redis存储用户的身份令牌有以下作用:
1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。
2、由于jwt令牌过长,不宜存储在cookie中,所以将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存储。
将认证服务changgou_user_auth中的application.yml配置文件中的Redis配置改成自己对应的端口和密码。
认证服务需要实现的功能如下:
1、登录接口
前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌存储到redis。 将令牌写入cookie。
2、退出接口 校验当前用户的身份为合法并且为已登录状态。 将令牌从redis删除。 删除cookie中的令牌。
修改changgou_user_auth中application.yml配置文件,修改对应的授权配置
auth:
ttl: 1200 #token存储到redis的过期时间
clientId: changgou #客户端ID
clientSecret: changgou #客户端秘钥
cookieDomain: localhost #Cookie保存对应的域名
cookieMaxAge: -1 #Cookie过期时间,-1表示浏览器关闭则销毁
为了不破坏Spring Security的代码,我们在Service方法中通过RestTemplate请求Spring Security所暴露的申请令 牌接口来申请令牌,下边是测试代码:
@SpringBootTest
@RunWith(SpringRunner.class)
public class TokenTest {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
/****
* 发送Http请求创建令牌
*/
@Test
public void testCreateToken() throws InterruptedException {
//采用客户端负载均衡,从eureka获取认证服务的ip 和端口
ServiceInstance serviceInstance = loadBalancerClient.choose("USER-AUTH");
URI uri = serviceInstance.getUri();
//申请令牌地址
String authUrl = uri + "/oauth/token";
//1、header信息,包括了http basic认证信息
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
//进行Base64编码,并将编码后的认证数据放到头文件中
String httpbasic = httpbasic("changgou", "changgou");
headers.add("Authorization", httpbasic);
//2、指定认证类型、账号、密码
MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();
body.add("grant_type","password");
body.add("username","itheima");
body.add("password","123456");
HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new HttpEntity<MultiValueMap<String, String>>(body, headers);
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
//远程调用申请令牌
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class);
Map result = exchange.getBody();
System.out.println(result);
}
/***
* base64编码
* @param clientId
* @param clientSecret
* @return
*/
private String httpbasic(String clientId,String clientSecret){
//将客户端id和客户端密码拼接,按“客户端id:客户端密码”
String string = clientId+":"+clientSecret;
//进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
}
AuthService接口:
public interface AuthService {
AuthToken login(String username, String password, String clientId, String clientSecret);
}
AuthServiceImpl实现类:
基于刚才写的测试实现申请令牌的service方法如下:
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.ttl}")
private long ttl;
/**
* 申请令牌
* @param username
* @param password
* @param clientId
* @param clientSecret
* @return
*/
@Override
public AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
URI uri = serviceInstance.getUri();
String url = uri+"/oauth/token";
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username",username);
body.add("password",password);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body,headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseEntity.getBody();
if (map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){
throw new RuntimeException("申请令牌失败");
}
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) map.get("access_token"));
authToken.setRefreshToken((String) map.get("refresh_token"));
authToken.setJti((String) map.get("jti"));
stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);
return authToken;
}
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId+":"+clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
AuthController编写用户登录授权方法,代码如下:
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private AuthService authService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Value("${auth.cookieDomain}")
private String cookieDomain;
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@PostMapping("/login")
public Result login(String username,String password){
if (StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不存在");
}
if (StringUtils.isEmpty(password)){
throw new RuntimeException("密码不存在");
}
AuthToken authToken = authService.applyToken(username,password,clientId,clientSecret);
this.saveJtiToCookie(authToken.getJti());
return new Result(true, StatusCode.OK,"登录成功");
}
private void saveJtiToCookie(String jti) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);
}
}
修改认证服务WebSecurityConfig类中configure(),添加放行路径
使用postman测试:
1)Post请求:http://localhost:9200/oauth/login
当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是itheima都可以访问。 因此需要动态获取用户帐号与密码.
用户微服务对外暴露根据用户名获取用户信息接口
@GetMapping("/load/{username}")
public User findUserInfo(@PathVariable("username") String username){
return userService.findById(username);
}
changgou_user_server_api新增feign接口
@FeignClient(name="user")
public interface UserFeign {
@GetMapping("/user/load/{username}")
public User findUserInfo(@PathVariable("username") String username);
}
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_user_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
@EnableFeignClients(basePackages = "com.changgou.user.feign")
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
<version>2.1.3.RELEASEversion>
dependency>
dependencies>
@SpringBootApplication
@EnableDiscoveryClient
public class WebGateWayApplication {
public static void main(String[] args) {
SpringApplication.run(WebGateWayApplication.class,args);
}
}
spring:
application:
name: gateway-web
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: changgou_goods_route
uri: lb://goods
predicates:
- Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**
filters:
#- PrefixPath=/brand
- StripPrefix=1
#用户微服务
- id: changgou_user_route
uri: lb://user
predicates:
- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1
#认证微服务
- id: changgou_oauth_user
uri: lb://user-auth
predicates:
- Path=/api/oauth/**
filters:
- StripPrefix=1
redis:
host: 192.168.200.128
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true
业务逻辑:
1)判断当前请求是否为登录请求,是的话,则放行
3)判断redis中令牌是否存在,没有的话,拒绝访问
@Component
public class AuthFilter implements GlobalFilter, Ordered {
public static final String Authorization = "Authorization";
@Autowired
private AuthService authService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取当前请求对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
if ("/api/oauth/login".equals(path)){
//放行
return chain.filter(exchange);
}
//判断cookie上是否存在jti
String jti = authService.getJtiFromCookie(request);
if (StringUtils.isEmpty(jti)){
//拒绝访问,请求跳转
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//判断redis中token是否存在
String redisToken = authService.getTokenFromRedis(jti);
if (StringUtils.isEmpty(redisToken)){
//拒绝访问,请求跳转
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//校验通过 , 请求头增强,放行
request.mutate().header(Authorization,"Bearer "+redisToken);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
新建业务逻辑类AuthService
@Service
public class AuthService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 判断cookie中jti是否存在
* @param request
* @return
*/
public String getJtiFromCookie(ServerHttpRequest request) {
HttpCookie cookie = request.getCookies().getFirst("uid");
if (cookie!=null){
return cookie.getValue();
}
return null;
}
/**
* 判断redis中令牌是否过期
* @param jti
* @return
*/
public String getTokenFromRedis(String jti) {
String token = stringRedisTemplate.boundValueOps(jti).get();
return token;
}
}
测试:
清除postman中的cookie数据,在未登录的情况下,并访问: http://localhost:8001/api/user 。会返回401异常信息
访问:http://localhost:8001/api/oauth/login,并重新测试。可以发现测试通过,拿到返回结果数据
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
修改WebSecurityConfig类
web.ignoring().antMatchers(
"/oauth/login",
"/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
@Controller
@RequestMapping("/oauth")
public class LoginRedirectController {
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
<script th:inline="javascript">
var app = new Vue({
el:"#app",
data:{
username:"",
password:"",
msg:""
},
methods:{
login:function(){
app.msg="正在登录";
axios.post("/api/oauth/login?username="+app.username+"&password="+app.password).then(function (response) {
if (response.data.flag){
app.msg="登录成功";
}else{
app.msg="登录失败";
}
})
}
}
})
</script>
定义路径过滤
public class URLFilter {
public static String filterPath = "/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
public static boolean hasAuthorize(String url){
String[] split = filterPath.replace("**", "").split(",");
for (String value : split) {
if (url.startsWith(value)){
return true;
}
}
return false;
}
}
访问:http://localhost:9200/oauth/toLogin
用户每次访问微服务的时候,先去oauth2.0服务登录,登录后再访问微服务网关,微服务网关将请求转发给其他微服务处理。
由于我们项目使用了微服务,任何用户都有可能使用任意微服务,此时我们需要控制相关权限,例如:普通用户角色不能使用用户的删除操作,只有管理员才可以使用,那么这个时候就需要使用到SpringSecurity的权限控制功能了。
注解方式
直接在上面配置一段@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
用于开启@PreAuthorize的支持,代码如下:
package com.changgou.user.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
然后就可以在controller类上加入@PreAuthorize
如果希望一个方法能被多个角色访问,配置:@PreAuthorize("hasAnyAuthority('admin','user')")
如果希望一个类都能被多个角色访问,在类上配置:@PreAuthorize("hasAnyAuthority('admin','user')")
pox.xml
<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>changgou_parentartifactId>
<groupId>com.changgougroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>changgou-user-oauthartifactId>
<description>
OAuth2.0认证环境搭建
description>
<dependencies>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_common_dbartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-dataartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_user_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
project>
yml
server:
port: 9200
spring:
application:
name: user-auth
redis:
host: 192.168.200.128
port: 6379
password:
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.128:3306/changgou_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
username: root
password: root
main:
allow-bean-definition-overriding: true
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
auth:
ttl: 3600 #token存储到redis的过期时间
clientId: changgou
clientSecret: changgou
cookieDomain: localhost
cookieMaxAge: -1
encrypt:
key-store:
location: classpath:/changgou.jks
secret: changgou
alias: changgou
password: changgou
util中的
AuthToken
package com.changgou.oauth.util;
import java.io.Serializable;
public class AuthToken implements Serializable{
//令牌信息 jwt
String accessToken;
//刷新token(refresh_token)
String refreshToken;
//jwt短令牌
String jti;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
}
CookieUtil
package com.changgou.oauth.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class CookieUtil {
/**
* 设置cookie
*
* @param response
* @param name cookie名字
* @param value cookie值
* @param maxAge cookie生命周期 以秒为单位
*/
public static void addCookie(HttpServletResponse response, String domain, String path, String name,
String value, int maxAge, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
/**
* 根据cookie名称读取cookie
* @param request
* @return map
*/
public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
Map<String,String> cookieMap = new HashMap<String,String>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
String cookieValue = cookie.getValue();
for(int i=0;i<cookieNames.length;i++){
if(cookieNames[i].equals(cookieName)){
cookieMap.put(cookieName,cookieValue);
}
}
}
}
return cookieMap;
}
}
UserJwt
package com.changgou.oauth.util;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class UserJwt extends User {
private String id; //用户ID
private String name; //用户名字
public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
启动类
package com.changgou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.changgou.auth.dao")
@EnableFeignClients(basePackages = {"com.changgou.user.feign"})
public class OAuthApplication {
public static void main(String[] args) {
SpringApplication.run(OAuthApplication.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
重点的config
AuthorizationServerConfig
package com.changgou.oauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
@Autowired
private DataSource dataSource;
//jwt令牌转换器
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//SpringSecurity 用户自定义授权认证类
@Autowired
UserDetailsService userDetailsService;
//授权认证管理器
@Autowired
AuthenticationManager authenticationManager;
//令牌持久化存储接口
@Autowired
TokenStore tokenStore;
@Autowired
private CustomUserAuthenticationConverter customUserAuthenticationConverter;
/***
* 客户端信息配置
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).clients(clientDetails());
}
/***
* 授权服务器端点配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)//认证管理器
.tokenStore(tokenStore) //令牌存储
.userDetailsService(userDetailsService); //用户信息service
}
/***
* 授权服务器的安全配置
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients()
.passwordEncoder(new BCryptPasswordEncoder())
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
//读取密钥的配置
@Bean("keyProp")
public KeyProperties keyProperties(){
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
//客户端配置
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
@Autowired
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/****
* JWT令牌转换器
* @param customUserAuthenticationConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
keyProperties.getKeyStore().getLocation(), //证书路径 changgou.jks
keyProperties.getKeyStore().getSecret().toCharArray()) //证书秘钥 changgouapp
.getKeyPair(
keyProperties.getKeyStore().getAlias(), //证书别名 changgou
keyProperties.getKeyStore().getPassword().toCharArray()); //证书密码 changgou
converter.setKeyPair(keyPair);
//配置自定义的CustomUserAuthenticationConverter
DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
return converter;
}
}
CustomUserAuthenticationConverter
package com.changgou.oauth.config;
import com.changgou.oauth.util.UserJwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Autowired
UserDetailsService userDetailsService;
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap response = new LinkedHashMap();
String name = authentication.getName();
response.put("username", name);
Object principal = authentication.getPrincipal();
UserJwt userJwt = null;
if(principal instanceof UserJwt){
userJwt = (UserJwt) principal;
}else{
//refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
userJwt = (UserJwt) userDetails;
}
response.put("name", userJwt.getName());
response.put("id", userJwt.getId());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
UserDeatilsServiceImpl
package com.changgou.oauth.config;
import com.changgou.oauth.util.UserJwt;
import com.changgou.user.feign.UserFeign;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/*****
* 自定义授权认证类
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
@Autowired
private UserFeign userFeign;
/****
* 自定义授权认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if(authentication==null){
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if(clientDetails!=null){
//秘钥
String clientSecret = clientDetails.getClientSecret();
//静态方式
//return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
//数据库查找方式
return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
//根据用户名查询用户信息
//String pwd = new BCryptPasswordEncoder().encode("itheima");
com.changgou.user.pojo.User user = userFeign.findUserInfo(username);
//创建User对象
String permissions = "goods_list,seckill_list";
UserJwt userDetails = new UserJwt(username,user.getPassword(),AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
return userDetails;
}
}
WebSecurityConfig
package com.changgou.oauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/***
* 忽略安全拦截的URL
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/login",
"/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
}
/***
* 创建授权管理认证对象
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/***
* 采用BCryptPasswordEncoder对密码进行编码
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/****
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic() //启用Http基本身份验证
.and()
.formLogin() //启用表单身份验证
.and()
.authorizeRequests() //限制基于Request请求访问
.anyRequest()
.authenticated(); //其他请求都需要经过验证
//开启表单登录
http.formLogin().loginPage("/oauth/toLogin")//设置访问登录页面的路径
.loginProcessingUrl("/oauth/login");//设置执行登录操作的路径
}
}
AuthService
package com.changgou.oauth.service;
import com.changgou.oauth.util.AuthToken;
public interface AuthService {
AuthToken login(String username,String password,String clientId,String clientSecret);
}
AuthServiceImpl
package com.changgou.oauth.service.impl;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.ttl}")
private long ttl;
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//1.申请令牌
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
URI uri = serviceInstance.getUri();
String url=uri+"/oauth/token";
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username",username);
body.add("password",password);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));
HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseEntity.getBody();
if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){
//申请令牌失败
throw new RuntimeException("申请令牌失败");
}
//2.封装结果数据
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) map.get("access_token"));
authToken.setRefreshToken((String) map.get("refresh_token"));
authToken.setJti((String)map.get("jti"));
//3.将jti作为redis中的key,将jwt作为redis中的value进行数据的存放
stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);
return authToken;
}
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId+":"+clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
AuthController
package com.changgou.oauth.controller;
import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import com.changgou.oauth.util.CookieUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private AuthService authService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Value("${auth.cookieDomain}")
private String cookieDomain;
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@RequestMapping("/login")
@ResponseBody
public Result login(String username, String password, HttpServletResponse response){
//校验参数
if (StringUtils.isEmpty(username)){
throw new RuntimeException("请输入用户名");
}
if (StringUtils.isEmpty(password)){
throw new RuntimeException("请输入密码");
}
//申请令牌 authtoken
AuthToken authToken = authService.login(username, password, clientId, clientSecret);
//将jti的值存入cookie中
this.saveJtiToCookie(authToken.getJti(),response);
//返回结果
return new Result(true, StatusCode.OK,"登录成功",authToken.getJti());
}
//将令牌的断标识jti存入到cookie中
private void saveJtiToCookie(String jti, HttpServletResponse response) {
CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);
}
}
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
<version>2.1.3.RELEASEversion>
dependency>
dependencies>
yml
spring:
application:
name: gateway-web
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: changgou_goods_route
uri: lb://goods
predicates:
- Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**
filters:
#- PrefixPath=/brand
- StripPrefix=1
#用户微服务
- id: changgou_user_route
uri: lb://user
predicates:
- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1
#认证微服务
- id: changgou_oauth_user
uri: lb://user-auth
predicates:
- Path=/api/oauth/**
filters:
- StripPrefix=1
redis:
host: 192.168.200.128
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true
filter中的
AuthFilter
package com.changgou.web.gateway.filter;
import com.changgou.web.gateway.service.AuthService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private AuthService authService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//1.判断当前请求路径是否为登录请求,如果是,则直接放行
String path = request.getURI().getPath();
if ("/api/oauth/login".equals(path) || !UrlFilter.hasAuthorize(path) ){
//直接放行
return chain.filter(exchange);
}
//2.从cookie中获取jti的值,如果该值不存在,拒绝本次访问
String jti = authService.getJtiFromCookie(request);
if (StringUtils.isEmpty(jti)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//3.从redis中获取jwt的值,如果该值不存在,拒绝本次访问
String jwt = authService.getJwtFromRedis(jti);
if (StringUtils.isEmpty(jwt)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//4.对当前的请求对象进行增强,让它会携带令牌的信息
request.mutate().header("Authorization","Bearer "+jwt);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
UrlFilter
package com.changgou.web.gateway.filter;
public class UrlFilter {
//所有需要传递令牌的地址
public static String filterPath="/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
public static boolean hasAuthorize(String url){
String[] split = filterPath.replace("**", "").split(",");
for (String value : split) {
if (url.startsWith(value)){
return true; //代表当前的访问地址是需要传递令牌的
}
}
return false; //代表当前的访问地址是不需要传递令牌的
}
}
AuthService
package com.changgou.web.gateway.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//从cookie中获取jti的值
public String getJtiFromCookie(ServerHttpRequest request) {
HttpCookie httpCookie = request.getCookies().getFirst("uid");
if (httpCookie != null){
String jti = httpCookie.getValue();
return jti;
}
return null;
}
//从redis中获取jwt
public String getJwtFromRedis(String jti) {
String jwt = stringRedisTemplate.boundValueOps(jti).get();
return jwt;
}
}