什么是用户身份认证?
用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。
什么是用户授权?
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。
5张表
user
user_role
role
role_auth
auth
让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。
当需要访问第三方系统的资源时需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。
好处:
1用户:防止信息泄露
2网站:自己就不用再做一套登陆系统。
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图:
特点:
1、认证系统为独立的系统。
2、各子系统通过Http或其它协议与认证系统通信,完成用户认证。
3、用户身份信息存储在Redis集群。
Java中有很多用户认证的框架都可以实现单点登录:
1、Apache Shiro.
2、CAS
3、Spring security CAS
为什么:
是什么:
怎么用:
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的 接口协议。 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认证的例子,黑马程序员网站使用微信认证的过程:
流程图:
4399——————》微信第三方登录
1.客户端请求第三方授权
用户进入某网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfTRFypm-1606806084061)(img/1564717364846.png)]
点击“用QQ账号登录”出现一个二维码,此时用户扫描二维码,开始授权。
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,QQ会询问用户是否给授权黑马程序员访问自己的QQ数据,用户点击“确认登录”表示同意授权,QQ认证服务器会 颁发一个授权码,并重定向到网站。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dF4gGuc0-1606806084063)(img/1564717400429.png)]
3.客户端获取到授权码,请求认证服务器申请令牌 此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
4.认证服务器向客户端响应令牌 认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功。
5.客户端请求资源服务器的资源 客户端携带令牌访问资源服务器的资源。 黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息。
6.资源服务器返回受保护资源 资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。 注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常要请求认证 服务器来校验令牌的合法性。
Oauth2.0认证流程如下: 引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
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协议,下图是项目认证架构图:
登录:
1、用户请求认证服务完成认证。
2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。
访问:
1、用户携带令牌请求资源服务,请求资源服务必先经过网关。
2、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。
3、资源服务获取令牌,根据令牌完成授权。
4、资源服务完成授权则响应资源信息。
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
优点:
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 createJWT() {
// 基于私钥生成jwt
// 1创建秘钥工厂
// 1.1秘钥位置
ClassPathResource classPathResource = new ClassPathResource("changgou.jks");
//1.2秘钥库密码
String keyPass = "changgou";
/**
* 1秘钥位置
* 2秘钥库密码
*/
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, keyPass.toCharArray());
// 2基于工厂拿到私钥
String alias="changgou";
String password="changgou";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
//转化为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey)keyPair.getPrivate();
// 3生成jwt
Map<String,String> map=new HashMap<>();
map.put("conpany", "itheima");
map.put("address", "taiyuan");
Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));
String jwtEncoded = jwt.getEncoded();
System.out.println("jwtEncoded:"+jwtEncoded);
String claims = jwt.getClaims();
System.out.println("claims:"+claims);
}
}
运行 查看输出
访问 http://tool.chinaz.com/Tools/Base64.aspx 解析base64
上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。
在changgou-user-oauth创建测试类com.changgou.token.ParseJwtTest实现解析校验令牌数据,代码如下:
public class ParseJwtTest {
@Test
public void parseJwt(){
//基于公钥去解析jwt
String jwt ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYmVpamluZyIsImNvbXBhbnkiOiJoZWltYSJ9.cjZNz8G0m4noNYN2VM1SH3ujAtbHElW5Vtbadb0NDI0cjM1DaAXzMA53Qbj4pmVQPl_IfSKqUEXbLxowdRa5NHR43laFsR0kzGbJiTINfSVSroSslYpDdEVwCeAF_a7I-R819YTj4p6sjuYKXbzXpeZQErczFbWWWGR2_U44xH6u1ejRNv8PikFiuzNw-muL7zUJkvqeSJzbEMnQdZMbfvZp4LtSI6B4G_PqpdNXkv19-juxAh99VgJInH_ItF0y5IBOxofA7gRebCZmU8L57gO9ohf2L00D95kis_Ji8lmA1ptLIfXqO_qLVvLBUNH-VtgjGAF0-0pyB-5jlbHP7w";
String publicKey ="-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";
//解析令牌
Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));
//获取负载
String claims = token.getClaims();
System.out.println(claims);
}
}
运行验证
改错公钥或令牌,再测试
观察表结构及数据
oauth框架必须有的表
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、资源服务器返回受保护资源
浏览器访问 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参数(授权码)
首先跳转到登录页面:
用户名密码都填为:changgou
点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=k45iLY就是返回的授权码, 每一个授权码只能使用一次
注意观察:此接口为oauth2提供,项目中没有写controller。
拿到授权码后,申请令牌。
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的唯一标识
思考:
cookie redis放那些令牌?
cookie 长度限制的 4K。
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:密码
并且此链接需要使用 http Basic认证。
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:
上图的业务流程如下:
1、客户端请求认证服务申请令牌
2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer令牌。
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息
基本上所有微服务都是资源服务,这里我们在课程管理服务上配置授权控制,当配置了授权控制后如要访问课程信 息则必须提供令牌。
1、配置公钥 ,将 changggou_user_auth 项目中public.key复制到changgou_service_user中
<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","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
1不携带令牌访问http://localhost:9005/user
由于该地址受访问限制,需要授权,所以出现如下错误:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
2携带令牌访问http://localhost:9005/user
在http header中添加 Authorization: Bearer 令牌
当输入错误的令牌也无法正常访问资源。
功能流程图如下:
1登陆:认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie
2访问:用户访问资源页面,带着cookie到网关,网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
3退出:请求认证服务,清除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表示浏览器关闭则销毁
配置redis
redis:
host: 192.168.200.128
明确:request=请求头+请求体
所以postman
转化为:请求头中
计算方法为:Basic base64(客户端名称:客户端密码)
为了不破坏Spring Security的代码,我们在Service方法中通过RestTemplate请求Spring Security所暴露的申请令 牌接口来申请令牌,下边是测试代码:
启动类添加
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
写测试类
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplyTokenTest {
@Autowired
private RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
// 用户名密码方式获取令牌
@Test
public void applyToken() {
//请求地址: http://localhost:9200/oauth/token
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
// http://localhost:9200
URI uri = serviceInstance.getUri();
String url = uri + "/oauth/token";
//请求体 body
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", "itheima");
body.add("password", "itheima");
//请求头
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", this.getHttpBasic("changgou", "changgou"));
//封装请求参数
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
//后端401 400,不认为是异常,直接返回给前端
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if(response.getRawStatusCode()!=400&&response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
/**
* 1 url
* 2请求方法
* 3请求头和体
* 4返回值类型
*/
ResponseEntity<Map> responseMap = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseMap.getBody();
System.out.println(map);
}
//请求头中Authorization值的计算方法
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId + ":" + clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
1service层添加接口 com.changgou.oauth.service
public interface AuthService {
AuthToken login(String username,String password,String clientId,String clientSecret);
}
2接口实现 com.changgou.oauth.service.impl
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
RedisTemplate redisTemplate;
@Value("${auth.ttl}")
private long ttl;
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
// 1申请令牌
//请求地址: http://localhost:9200/oauth/token
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
// http://localhost:9200
URI uri = serviceInstance.getUri();
String url = uri + "/oauth/token";
//请求体 body
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);
//后端401 400,不认为是异常,直接返回给前端
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if(response.getRawStatusCode()!=400&&response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
/**
* 1 url
* 2请求方法
* 3请求头和体
* 4返回值类型
*/
ResponseEntity<Map> responseMap = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseMap.getBody();
if(map==null&&map.get("access_token")==null&&map.get("refresh_token")==null&&map.get("jti")==null){
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:jwt存储到redis
redisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl , TimeUnit.SECONDS);
return authToken;
}
//请求头中Authorization值的计算方法
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId + ":" + clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
com.changgou.oauth.controller 添加
@Controller
@RequestMapping("/oauth")
public class AuthController {
@Autowired
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("/login")
@ResponseBody
private Result login(String username, String password, HttpServletResponse response){
// 1校验参数
if (StringUtils.isEmpty(username)){
throw new RuntimeException("请输入用户名!");
}
if (StringUtils.isEmpty(password)){
throw new RuntimeException("请输入用密码!");
}
// 2申请令牌
AuthToken authToken = authService.login(username, password, clientId, clientSecret);
// 3将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);
}
}
拓展:cookie session 区别和联系 应用场景。
如何用代码来操作cookie session?
修改认证服务WebSecurityConfig类中configure(),添加放行路径
使用postman测试:
1重启认证服务
2postman测试
POST http://localhost:9200/oauth/login
username itheima
password itheima
观察cookie
观察redis中数据
若key value有乱码,则使用 StringRedisTemplate。
1需求:
当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是itheima都可以访问。因此需要动态获取用户帐号与密码.
2用户服务 com.changgou.user.controller UserController中新增方法
@GetMapping("/load/{username}")
public User findUserInfo(@PathVariable("username") String username){
User user = userService.findById(username);
return user;
}
3user-api中 新增 feigh客户端。com.changgou.user.feign
@FeignClient(name = "user")
public interface UserFeign {
@GetMapping("/user/load/{username}")
public User findUserInfo(@PathVariable("username") String username);
}
4认证服务导入依赖
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_user_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
5认证启动类添加注解
@EnableFeignClients(basePackages = {"com.changgou.user.feign"})
6修改com.changgou.oauth.config UserDetailsServiceImpl 改为动态获取用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
@Autowired
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;
}
}
7重启测试 登陆 http://localhost:9200/oauth/login
报错401 因为用户服务已经受保护。
8用户服务 com.changgou.user.config ResourceServerConfig类的configure方法
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
9再次访问即可成功
<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>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
dependencies>
创建changgou_gateway_web客户端网关模块。不用添加依赖,因为父依赖已有。
启动类com.changgou.web.gateway
@SpringBootApplication
@EnableEurekaClient
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需求:
登陆时:放行
用户访问时:
2客户端网关工程 com.changgou.web.gateway.filter
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
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)) {
//放行
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;
}
}
3com.changgou.web.gateway.service
@Service
public class AuthService {
@Autowired
StringRedisTemplate stringRedisTemplate;
//从request中获取jti
public String getJtiFromCookie(ServerHttpRequest request) {
HttpCookie httpCookie = request.getCookies().getFirst("uid");
if (httpCookie != null) {
String jti = httpCookie.getValue();
return jti;
}
return null;
}
public String getJwtFromRedis(String jti) {
String jwt = stringRedisTemplate.boundValueOps(jti).get();
return jwt;
}
}
测试:
1启动用户网关
2测试Get http://localhost:8001/api/user/
401
3测试登陆
POST http://localhost:8001/api/oauth/login?username=heima&password=123456
4再测试Get http://localhost:8001/api/user/ 成功。因为查看cookie中有uid。
需求:oauth自带登陆页面太丑,我们想要一个体验更好的登陆页面。
访问路径: http://localhost:8001/api/oauth/toLogin
流程:客户端网关-》认证服务controller-》login.html。
点击登陆-》/api/oauth/login-》获取令牌登陆
1 认证服务添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
2 resources下 新增static、templates文件夹。放入资源里的数据。
3 AuthController中添加跳转
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
4 放行静态资源。com.changgou.oauth.config包 WebSecurityConfig类
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers( "/oauth/login","/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**"
);
}
5登陆经过用户网关,所以将static目录copy至gateway_web一份
6开启表单登陆。com.changgou.oauth.config包 WebSecurityConfig类
@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");//设置执行登陆操作的路径
}
7login.html修改
101 <input id="inputName" type="text" v-model="username" placeholder="邮箱/用户名/手机号" class="span2 input-xfat">
104 <input id="inputPassword" type="password" v-model="password" placeholder="请输入密码" class="span2 input-xfat">
114-115
<button class="sui-btn btn-block btn-xlarge btn-danger" type="button" @click="login()">登 录button>
167-188
<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>
测试:
1geteway-web工程 com.changgou.web.gateway.filter AuthFilter。放行登陆跳转。
//1判断当前请求是否为登陆,是就放行
String path = request.getURI().getPath();
if ("/api/oauth/login".equals(path) || "/api/oauth/toLogin".equals(path) ) {
//放行
return chain.filter(exchange);
}
2重启认证服务、客户网关
3 浏览器访问 http://localhost:8001/api/oauth/toLogin
输入heima 123456 ,显示登陆成功。
5F12查看浏览器cookie
6查看redis
1需求:许多url我们需要经行令牌校验,许多url不需要。所以写一个工具类判断。
2写一个工具类,判断url是否需要过滤。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; //代表当前的访问地址是不需要传递令牌的
}
}
3AuthFilter优化
if ("/api/oauth/login".equals(path) || !UrlFilter.hasAuthorize(path)) {
//放行
return chain.filter(exchange);
}
总结:
1登陆
登陆鉴权 --》》》5张表 user role auth
单点登录—》》 session cookie
第三方登录----》oauth2
2jwt回顾
组成 3部分 头 负载 签名
命令行 生成私钥库 公钥
代码 jwtHelper
3oatth2
auth微服务 postman 申请授权码 申请令牌 校验令牌 刷新令牌
密码模式
4微服务改造oauth
public.key 导包 配置文件
请求头 authirization : bearer jwt
5认证开发
登录接口
restTemplate.exchange(url,post,entity,map)
controller service
6动态获取真实用户