SpringSecurityOauth2
- 用户认证需求分析
- 用户认证与授权
- 单点登录需求
- 第三方认证需求
- 用户认证技术方案
- 单点登录技术方案
- Oauth2认证
- Oauth2认证流程
- 2.2.2Oauth2在本项目中的应用
- SpringSecurity Oauth2认证解决方案
- SpringSecurityOauth2研
- 目标
- 搭建认证服务器
- 导入基础工程
- 创建数据库
- Oauth2授权码模式
- Oauth2授权码模式
- 授权码授权流程
- 申请授权码
- 申请令牌
- 资源服务授权
- 资源服务授权流程
- 资源服务授权配置
- 资源服务授权测试
- 解决swagger-ui无法访问
- Oauth2密码模式授权
- 校验令牌
- 刷新令牌
- JWT研究
- JWT介绍
- 令牌结构
- JWT入门
- 生成私钥和公钥
- 生成JWT令牌
- 验证JWT令牌
- 认证接口开发
- 需求分析
- Redis配置
- 安装Redis
- redis连接配置
- 测试
- 认证服务
- 需求分析
- Api接口
- 配置参数
- 申请令牌测试
- Dao
- Service
- Controller
- 登录url放行
- 测试认证接口
- 测试写入Cookie
用户认证需求分析
无论在什么时候,都要将文字信息图像化在自己的大脑中,将之连接贯通这样子的效果才是最好的用户认证与授权
截至目前,项目已经完成了在线学习功能,用户通过在线学习页面点播视频进行学习,如何去记录学生的学习过程呢?逍遥掌握学生的学习情况就需要知道用户的身份信息,记录那个用户在什么时间学习社么课程;如果用户要够吗i课程也需要知道用户的身份信息,所以,去管理学生的学习过程最基本的要实现省份认证.
什么是身份认证?
用户身份认证及用户取访问系统容资源是系统要求验证用户的身份信息,身份合法放可以继续访问,常见的用户身份信息表现形式有:用户名密码登录,指纹打卡等方式.
什么是用户授权?
用户认证通古今哦后取访问系统的资源,系统会判断用户是否拥有访问资源的全新啊,只允许有全新啊的系统资源,没有权限的资源将无法访问,这个过程叫用户授权.
单点登录需求
本项目包括了多个子项目,如:学习系统,教学管理中心,系统管理中心等,为了提高用户体验性,需要实习那用户之认证一次便可以在多个拥有访问权限的系统中访问,这个功能叫做单点登录.
引用百度百科:单点登录(SingleSign On),简称SSo 是目前比较流行的企业业务整合的解决方案之一.
SSO的定义实在多个应用系统中,用户只需要登录一次就可以访问所有的相互信任的应用系统中
下图是SSo的示意图,用户登录学成网,一次即可访问多个系统.
第三方认证需求
作为互联网项目,难免需要访问外部系统的资源,同样本系统也要访问第三方系统的资源接口,一个场景如下:
一个微信用户没有学成在线注册,本系统可以通过请求微信系统来验证该用户的身份,通过验证之后,便可以在本系统中学习,他的基本流程如下:
从上图可以看出,微信不属于本系统,本系统并没有存储微信用户的账号,密码等信息,本系统如果想要获取该用户的基本信息,则需要首先通过微信的认证系统(微信认证)进行认证,微信认证通过之后,本系统便可以获取噶微信用户的基本信息,从而咋i本系统将该微信用户的头像,昵称等信息显示出来,改用户便不用在本系统注册却可以直接学习.
什么是第三方额认证 ?(跨平台认证)
当需要访问第三方的系统的资源时,需要首先通过第三方系统的认证(例如:微信认证),由于第三方系统对用户认证通过,并授权资源的访问权限.
用户认证技术方案
单点登录技术方案
分布式西药要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在大度的储存介质中,比如MySql,Redis,考虑性能要求,通常存储在Redis中,如下图:
单点登录的特点时:
1.认证系统为独立的系统.
2.各子系统通过Http或者其他协议与认真那个系统通信,完成用户的认证.
3.用户身份信息存储在Redis集群.
Java中有很多用户认证的框架都可以实现单点登录:
1.Apache Shiro
2.CAS
3.Spring Security CAS
Oauth2认证
Oauth2认证流程
第三方认证技术方案最主要的是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议.
Oauth认证服务,任何服务提供上都可以实现自身的Oauth认证服务,因而Oauth是开放的.业界提供了Oauth的多种实现,如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而Oauth是建议的,互联网很多服务如OpenAPi很多大公司如Goole,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
下边分析一个Oaauth2认证的例子,黑马程序员网站的使用微信认证的流程:
1.客户端请求第三方授权
用户进入黑马程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息资源的拥有者.
点击微信,出现一个二维码,此时用户扫描二维码,开始给黑马程序员授权.
2.资源拥有者同意给客户端授权
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对自由按拥有着的身份进行验证,验证通过之后,微信会寻味用户是否给授权黑马程序员访问自己的微信数据,用户点击确认登陆表示同意授权,微信认证服务器会办法一个授权码,并重定向到黑马程序员的网站.
3.客户端获取到授权码,请求整整服务器申请令牌
此后过程用户看不到,客户端应用程序请求证人服务,请求携带授权码.
4.认证服务器向客户端相应令牌
认证服务器验证了客户端请求的授权码,如果合法,则给客户端办法令牌,令牌是客户端访问资源的通行证.
此交互过程用户看不到,当客户端拿到令牌之后,用户在黑马程序员看到已经成功登录.
5.客户端请求资源服务器的资源
客户端携带令牌访问资源服务器的资源.
河马程序员网站携带令牌请求访问微信服务器获取用户的基本信息.
6.资源服务器返沪i受保护资源
资源服务器校验令牌的合法性,如果合法,则向用户相应资源信息内容.
注意:资源服务器和认证服务器可以是一个服务器也可以分开的服务,如果是分开的服务资源通常要请求认证服务器来校验令牌的合法性.
Oaoth2包含以下角色:
1.客户端:
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:学成在线Android客户端,学成在线Web客户端(浏览器端),微信客户端等.
2.资源拥有者
通常为用户,也可以是应用程序,技改资源的拥有者.
3.授权服务器(也称认证服务器)
用来对资源拥有的身份进行认证,对方问资源进行授权.客户端想要访问资源需要通过认证服务器由资源哟另有这授权后放可以访问.
4.资源服务器
存储资源的服务器,比如,学成网用户管理服务器处处了学成网用户的信息,学成网学生的学习信息,微信的资源服务存储了微信的用户信息等.客户端最总访问资源服务器获取资源信息.
系统想要访问其他第三方系统的资源的时候,就可以走oauth2协议
2.2.2Oauth2在本项目中的应用
Oauth2是一个标准的开放的协议,应用程序可以根据自己的要求去使用Oauth2,本姓穆使用Oauth2实现如下目标:
1.学成在线访问的第三方系统资源
2.外部系统ing访问学成在线的资源
3.学成前端(客户端)访问学成在吸纳微服务的资源.例如:微服务A访问微服务B的资源,B访问A的资源.
SpringSecurity Oauth2认证解决方案
本项目采用Spring Security+ Oauth完成用户认证及用户授权,Spring Security是一个强大的高度可定制的身份证验证和访问控制框架,SPirng Security框架集成了Oauth2协议,下图是项目认证架构图:
1.用户请求认账服务,完成认证.
2.认证服务下发用户身份令牌,拥有身份你令牌表示用户身份合法
3.用户携带令牌请求资源服务,请求资源服务必须先要经过网关.
4.网管校验用户省份令牌的合法性,不合法表示用户没有登录,如果合法,则放行继续访问.
5.资源服务获取令牌,根据令牌完成授权.
6.资源服务完成授权则相应资源信息.
SpringSecurityOauth2研
目标
本项目认证服务基于SPirngSecurityOauth2进行构建,并且在其基础上做了一些拓展,采用JWT令牌机制,并自定义了用户身份信息内容,本教程的主要目标是学习在项目中集成SPirngSecurityOauth2.的方法和流程,通过SPirngSecurity的研究需要达到以下目标:
1.理解Oauth2的授权码认证流程及密码的流程.
2.理解SPringSecurityOauth2的工作流程.
3.掌握资源服务器集成SpringSecuritykuangjia完成Oauth2认证的流程.
搭建认证服务器
导入基础工程
导入“资料”目录下的xc-service-ucenter-auth工程,该工程是基于SpringSecurityOauth2的一个二次封装的工程,导入此工程研究Oauth2认证流程。
创建数据库
导入资料目录下的xc_user.sql,创建用户数据库
以“oauth_”开头的表都是springSecurity自带的表。
本项目中springSecurity主要使用oauth_client_details表:
client_id:客户端idresource_ids:资源id(暂时不用)client_secret:客户端密码scope:范围
access_token_validity:访问token的有效期(秒)
refresh_token_validity:刷新token的有效期(秒)authorized_grant_type:授权类型,authorization_code,password,refresh_token,client_credentials
Oauth2授权码模式
Oauth2.有以下授权模式:
授权码模式(AuthorizationCode)
隐式授权模式(Implicit)
密码模式(ResourceOwnerPasswordCredentials)
客户端模式(ClientCredentials)
其中授权码模式和密码是模式应用较多,本小姐介绍授权码模式.
Oauth2授权码模式
上边例举的黑马程序员网站使用微信认证的过程就是授权码模式,流程如下:
1.客户端请求第三方认证授权
2.用户(资源拥有者)同意给客户端授权
3.客户端获取到授权码,请求认证服务器申请令牌
4.认证服务器向客户端响应令牌
5.客户端请求资源服务器的资源
授权码授权流程
亲故其认证服务获取授权码:
Get请求:
localhost:40400/auth/oauth/autorize?
client_id = XcWebApp&response_type=code&app&redirect_uri=http://localhost
参数列表如下:
client_id : 客户端id,和授权配置类中设置的客户端id一致.
response_type: 授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致.
redirect_uri:跳转uri,当授权码申请成功后会跳到此地址,并且在后边带上code参数(授权码).
首先跳转到登录页面:
申请授权码
请求认证服务获取授权码:Get请求:
localhost:40400/auth/oauth/authorize?
client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
接下来进入授权页面
点击“同意”。
接下来返回授权码:
认证服务携带授权码跳转redirect_ur
申请令牌
拿到授权码后,申请令牌。Post请求:http://localhost:40400/auth/oauth/token参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用httpBasic认证。什么是httpBasic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端,一个例子:
Authorization:BasicWGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=是用户名:密码的base64编码。认证失败服务端返回401Unauthorized
以上测试使用postman完成:httpbasic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
Post请求参数:
点击发送:
申请令牌成功:
资源服务授权
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法,则可成功访问资源服务中的资源,如下图:
上图的业务流程如下:
1.客户端请求认证服务申请令牌
2.认证服务生成令牌
认证服务采用非堆成加密算法,使用私钥生成令牌
3.客户端携带私钥访问资源令牌
客户端在Httpheader中添加:Authorization:bearer令牌
4资源服务请求认证服务校验令牌的有效性
资源服务接收到令牌,使用公钥验证令牌的合法性
5.令牌如果有效,资源服务响应客户端信息
资源服务授权流程
资源服务授权配置
基本上所有微服务都是资源服务,这里在课程管理服务商授权控制,当配置了授权之后,访问课程信息需要提供令牌
资源服务授权测试
解决swagger-ui无法访问
Oauth2密码模式授权
校验令牌
刷新令牌
JWT研究
JWT介绍
在介绍JWT之前,先看一下传统校验令牌的方法,如下图:
问题:
在传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并且根据令牌获取用户的相关信息,性能低下.
解决:
使用JWT的思路,用户认证通过会得到一个JWT的令牌,JWT的令牌里面包括了用户的信息,客户端只要携带JWT令牌访问资源服务,资源服务按照实现约定的算法进行验证,无需每次请求 认证服务完成授权.JWT令牌授权的过程:
JWT:
什么是JWT?
JSON Web Token(JWT)诗意个开放的行业标准准(RFC7519),他定义了一种简单的,自包含的协议格式,用于在通信双发传递json对象,床底的信息经过数字其那名可以被验证和信任.JWT可以使用HMAC算法或者使用RAS的公钥/私钥来针对签名,防止篡改.
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
1.jwt基于json,非常方便解析.
2.可以在令牌中自定义丰富的内容,易拓展
3.通过对称加密算法和数字签名技术,JWT防止篡改,安全性高.
缺点:
1、JWT令牌较长,占存储空间比较大。
令牌结构
通过学习JWT令牌结构为自定义jwt令牌打好基础。
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzzHeader
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMACSHA256或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入门
JWT令牌生成采用非堆成加密算法
下边命令生成密钥证书,采用RSA算法,每个整数包含公钥和私钥
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore
Keytool 是一个java提供的证书管理工具
-alias : 明耀的别名
-keyalg: 使用的hash的算法
-keypass :密钥的访问明码
-keystore: 密钥库文件名,xckeystore保存了生成的证书
_store pass: 密钥库的访问密码
查询证书的信息:
keytool -list -keystore xc.keystore
输入密钥即可得到内容
生成私钥和公钥
生成JWT令牌
在认证工程创建测试类,测试jwt令牌的生成与验证。
验证JWT令牌
认证接口开发
执行流程
1.用户挡路,请求认证服务
2.认证服务通过,生成jwt令牌,将jwt令牌及相关信息写入Redis, 并且将身份令牌写入cookie
3.用户访问资源页面,嗲这cookie到网关
4.网关从kookie获取token,并且查询Redis,校验Token,如果Token不存在,则拒绝访问,否则放行
5.用户退出,请求认证服务,清除redis中的token并且删除cookie总的token
使用redis存储用户的身份令牌有以下作用:
1.实现用户退出注销功能,服务端清楚令牌后,即使客户请求携带token也是无效的.
2.由于jwt令牌过长,也不宜存储在cookie中,所以将jwt零阿皮存储在redis,有客户端请求服务端获取并在客户端存储
需求分析
Redis配置
安装Redis
redis连接配置
测试
认证服务
需求分析
Api接口
package com.xuecheng.api.auth;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/27 10:39:15
*/
@Api(value = "用户认证",description = "用户认证接口")
public interface AuthControllerApi {
@ApiOperation("登录")
public LoginResult login(LoginResult loginResult);
@ApiOperation("退出")
public ResponseResult logout();
}
配置参数
server:
port: ${PORT:40401}
servlet:
context-path: /auth
spring:
application:
name: xc-service-ucenter-auth
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
jedis:
pool:
maxActive: 3
maxIdle: 3
minIdle: 1
maxWait: -1 #连接池最大等行时间 -1没有限制
datasource:
druid:
url: ${MYSQL_URL:jdbc:mysql://localhost:3306/xc_user?characterEncoding=utf-8}
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
initialSize: 5 #初始建立连接数量
minIdle: 5 #最小连接数量
maxActive: 20 #最大连接数量
maxWait: 10000 #获取连接最大等待时间,毫秒
testOnBorrow: true #申请连接时检测连接是否有效
testOnReturn: false #归还连接时检测连接是否有效
timeBetweenEvictionRunsMillis: 60000 #配置间隔检测连接是否有效的时间(单位是毫秒)
minEvictableIdleTimeMillis: 300000 #连接在连接池的最小生存时间(毫秒)
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: XcWebApp
clientSecret: XcWebApp
cookieDomain: xuecheng.com
cookieMaxAge: -1
encrypt:
key-store:
location: classpath:/xc.keystore
secret: xuechengkeystore
alias: xckey
password: xuecheng
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/,http://localhost:50102/eureka/}
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ip-address: ${IP_ADDRESS:127.0.0.1}
instance-id: ${spring.application.name}:${server.port} #指定实例id
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
申请令牌测试
package com.xuecheng.auth;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.client.XcServiceList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
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.test.context.junit4.SpringRunner;
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.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description 测试redisDesktop[
* @date 2023/1/17 13:59:30
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestClient {
// 用来请求eureka的负载均衡
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
RestTemplate restTemplate;
// 远程请求spring security获取令牌
@Test
public void testClient(){
// 从eureka总获取认证服务的地址(因为spring security在认证服务中)
// serviceInstance就是一个微服务的实例
// 从eureka中获取一个认证服务的实例地址
ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
// 此地址就是http://ip:port
URI uri = serviceInstance.getUri();
// 令牌申请的地址 http://localhost:40400/auth/oauth/token
String authUrl = uri+"/auth/oauth/token";
// 定义header
LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String httpBasic = getHttpBasic("XcWebApp", "XcWebApp");
header.add("Authorization",httpBasic);
// 定义body
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username","itcast");
body.add("password","123");
HttpEntity<MultiValueMap<String,String>> multiValueMapHttpEntity = new HttpEntity<>(null,null);
// 设置restTemplate远程嗲用用的时候,对400和401不让报错,正确返回数据
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode()!=400&&response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class);
// 申请令牌的信息
Map bodyMap = exchange.getBody();
System.out.println(bodyMap);
}
// 获取httpbasic的串
private String getHttpBasic(String clientId,String clientSecret){
String string = clientId+":"+clientSecret;
// 将字符串进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic"+new String(encode);
}
}
Dao
Service
package com.xuecheng.auth.service;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.ucenter.ext.AuthToken;
import com.xuecheng.framework.domain.ucenter.response.AuthCode;
import com.xuecheng.framework.exception.ExceptionCast;
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.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RestController;
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;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/27 15:49:19
*/
public class AuthService {
// 用来请求eureka的负载均衡
@Value("${auth.tokenValiditySeconds}")
int tokenValiditySeconds;
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RestTemplate restTemplate;
// 远程请求spring security获取令牌
// 用户认证来申请令牌,将令牌存储到redis
public AuthToken login(String username, String password, String clientId, String clientSecret) {
// 请求spring Security来申请令牌
AuthToken authToken = this.applyToken(username, password, clientId, clientSecret);
if (authToken==null){
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_ERROR);
}
// 用户身份的令牌
String access_token = authToken.getAccess_token();
// 存储到redis中的内容
String jsonString = JSON.toJSONString(authToken);
// 将令牌存储到redis
boolean result = this.saveToken(access_token, jsonString, tokenValiditySeconds);
if (!result){
ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_ERROR);
}
return authToken;
}
// 存储令牌到redis
/**
*
* @param access_token 用户身份令牌
* @param content 内容就是AuthToken对象的内容
* @param ttl 过期的时间
* @return
*/
private boolean saveToken(String access_token,String content,long ttl){
String key = "user_token" + access_token;
stringRedisTemplate.boundValueOps(key).set(content,ttl, TimeUnit.SECONDS);
// 如果失败了就会返回一个小于零的数字
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return expire>0;
}
// 申请令牌
private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
// 从eureka总获取认证服务的地址(因为spring security在认证服务中)
// serviceInstance就是一个微服务的实例
// 从eureka中获取一个认证服务的实例地址
ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
// 此地址就是http://ip:port
URI uri = serviceInstance.getUri();
// 令牌申请的地址 http://localhost:40400/auth/oauth/token
String authUrl = uri + "/auth/oauth/token";
// 定义header
LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String httpBasic = getHttpBasic(clientId, clientSecret);
header.add("Authorization", httpBasic);
// 定义body
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new HttpEntity<>(null, null);
// 设置restTemplate远程嗲用用的时候,对400和401不让报错,正确返回数据
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class);
// 申请令牌的信息
Map bodyMap = exchange.getBody();
if (bodyMap==null ||
bodyMap.get("access_token")==null ||
bodyMap.get("refresh_token")==null ||
bodyMap.get("jti")== null
){
return null;
}
AuthToken authToken = new AuthToken();
authToken.setAccess_token((String) bodyMap.get("jti"));//用户身份令牌
authToken.setRefresh_token((String) bodyMap.get("refresh_token"));//刷新令牌
authToken.setAccess_token((String) bodyMap.get("jti"));//jwt令牌
return authToken;
}
// 存储令牌到redis
// 获取httpbasic的串
private String getHttpBasic(String clientId, String clientSecret) {
String string = clientId + ":" + clientSecret;
// 将字符串进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic" + new String(encode);
}
}
Controller
package com.xuecheng.auth.controller;
import com.xuecheng.api.auth.AuthControllerApi;
import com.xuecheng.auth.service.AuthService;
import com.xuecheng.framework.domain.ucenter.ext.AuthToken;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.utils.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/27 15:45:10
*/
@RestController
@RequestMapping("/")
public class AuthController implements AuthControllerApi {
@Value("${auth.clientId}")
String clientId;
@Value("${auth.clientSecret}")
String clientSecret;
@Value("${auth.cookieDomain}")
String cookieDomain;
@Value("${auth.cookieMaxAge}")
int cookieMaxAge;
@Autowired
AuthService authService;
@Override
@PostMapping("/userlogin")
public LoginResult login(LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();
// 申请令牌
AuthToken authToken = authService.login(username,password,clientId,clientSecret);
// 用户身份令牌
String access_token = authToken.getAccess_token();
// 将令牌存储到cookie
this.saveCookie(access_token);
return new LoginResult(CommonCode.SUCCESS,access_token);
}
// 将令牌存储到cookie
private void saveCookie(String token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","uid",token,cookieMaxAge,false);
}
@Override
public ResponseResult logout() {
return null;
}
}