Oauth2认证如何应用于不同平台间用户身份验证?
- 内容介绍
- 文章标签
- 相关推荐
本文共计3390个文字,预计阅读时间需要14分钟。
1+简介1.1+基本概念+认证:用户访问系统资源时,系统需验证用户的身份信息,确保身份合法且合规。方式包括常见的账户密码登录、验证码登录、指纹登录等。+授权:用户认证后,系统根据用户权限访问相应资源。
1 简介 1.1 基本概念- 认证:用户访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,常见的账号密码登录,验证码登录,指纹登陆
- 授权:用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限
- 单点登录 SSO:用户在一个系统中登录,其他任意受信任的系统都可以访问,例如在京东主页登陆了,京东其他页面就不需要再登录,这个功能就叫单点登录。
- 第三方账号登录:第三方系统对用户认证通过
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中
Java中有很多用户认证的框架都可以实现单点登录:
- shiro
- CAS
- Spring security CAS
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认 证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的
例如qq音乐登录微信授权的过程:
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
2.1.1 Header头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
2.1.2 Payload第二部分是荷载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
例如
{
"sub": "1234567890",
"name": "张三",
"admin": true
}
2.1.3 Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
secret:签名所使用的密钥。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2.2 生成私钥公钥-运维
JWT令牌生成采用非对称加密算法
2.2.1 生成密钥证书keytool -genkeypair -alias xiaomanhms -keyalg RSA -keypass xiaomanhms -keystore xiaomanhms.jks -storepass xiaomanhms
查询证书信息:
openssl是一个加解密工具包,使用openssl来导出公钥信息。
安装 openssl:slproweb.com/products/Win32OpenSSL.html
cmd进入xiaomanhms.jks文件所在目录执行如下命令:
keytool -list -rfc --keystore xiaomanhms.jks | openssl x509 -inform pem -pubkey
公钥部分为
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行
将hms_user_auth的工程导入到项目中去
然后在父工程pom文件中加入
<module>hms-user-oauth</module>
2.2.3.2 认证服务中创建测试类
用私钥加密
/**
* @author cc
* 基于私钥生成jwt
*/
public class CreateJWTTest {
public static void main(String []args) {
// 1 创建秘钥工厂
ClassPathResource classPathResource = new ClassPathResource("xiaomanhms.jks");
// 2 秘钥库密码
String keyPass = "xiaomanhms";
/**
* 1秘钥位置
* 2秘钥库密码
*/
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, keyPass.toCharArray());
// 2基于工厂拿到私钥
String alias="xiaomanhms";
String password="xiaomanhms";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
//转化为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey)keyPair.getPrivate();
// 3生成jwt
Map<String,String> map = new HashMap<>();
map.put("conpany", "hbue");
map.put("address", "wuhan");
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);
}
}
公钥解密
/**
* @author cc
* 解析jwt令牌测试
*/
public class ParseJwtTest {
public static void main(String[] args) {
//基于公钥去解析jwt
String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoid3VoYW4iLCJjb25wYW55IjoiaGJ1ZSJ9.Zmo4igopc9mKBW4BnbIms4ux4fDq80iV0PvPbwegr9kWT_JP129WCVHn0BkXHTTfMGSmBAwg9XdzvldbWlgTn-IfLPFYfsRFsunwBxMMkqVi4R5IsHczYu-Js2Bg6lZpHwRlbjmyQU0TkzM6bJPnTIxhaxLto7OuLwpSrG27NZUC8BH3BSJPzIp-fzw9NJNrskdt9UWyHcuGmw0dmrFvNKvQ64vQE6_ns0tGHv1vNkkaJdsRbPKVQw-0JpXEbQOVrM3iTdBwbqqXzEkm5fpFEQJzT-SvRkJdRyulvEmS4XL4iO6qzQ3zRLDUoOhV5f3ApUSGfPExtexTcilLTFkiQA";
// 公钥
String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----";
//解析令牌
Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));
//获取负载
String claims = token.getClaims();
System.out.println(claims);
}
}
3 Oauth2.0
3.1 准备工作
导入表oauth_client_details
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条初始化数据,其中加密字符明文为xiaoman
INSERT INTO `oauth_client_details` VALUES ('userweb', NULL, '$2a$10$ejwy6rInOvFWF44gAAgpA.iPWNhmVo7WSG98Yvg4lY9gCmVhxkNRa', 'app', 'authorization_code,password,refresh_token,client_credentials', 'localhost', NULL, 43200, 43200, NULL, NULL);
3.2 Oauth2授权模式介绍
OAuth2.0协议一共支持 4 种不同的授权模式:
- 授权码模式:常见的第三方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用说这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,自己做前后端分离登录就可以采用这种模式。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
一般用的比较多的就是 授权码模式和密码模式
启动hms_user_oauth微服务
浏览器访问
localhost:9200/oauth/authorize?client_id=userweb&response_type=code&scop=app&redirect_uri=localhost
client_id为username, client_secret为password
登陆后会进入进入授权页面:
点击Authorize,相当于我们微信登录qq音乐扫码后点击确认登陆,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=9prf3B就是返回的授权码, 每一个授权码只能使用一次
注意观察:此接口为oauth2提供,项目中没有写controller。
拿到授权码后,申请令牌。
localhost:9200/oauth/token
参数
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 localhost:9200/oauth/check_token?token= [access_token]
刷新令牌是当令牌快过期时重新生成一个令牌,它与授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。
Post:localhost:9200/oauth/token
grant_type: 固定为 refresh_token
refresh_token:刷新令牌
3.2.2 密码模式密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌
3.2.2.1 申请授权码localhost:9200/oauth/token
此链接需要使用 localhost:9101/user/userList
2.携带令牌访问 localhost:9101/user/userList
在localhost:9200/oauth/token"; //不要写死,要部署多个服务 ServiceInstance serviceInstance = loadBalancerClient.choose("user_auth"); URI uri = serviceInstance.getUri(); //localhost:9200 String url=uri+"/oauth/token"; // 2 请求头 // 2.1 请求头 MultiValueMap<String, String> headers=new LinkedMultiValueMap<>(); headers.add("Authorization", getHttpHeaders("userweb","xiaoman")); // 2.2 请求体 MultiValueMap<String, String> body=new LinkedMultiValueMap<>(); body.add("grant_type","password"); body.add("username","userweb"); body.add("password","xiaoman"); // 400 401 不报错,返回回来 restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){ //响应当中有错误 如何处理 @Override public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { if(response.getRawStatusCode() != 400 && response.getRawStatusCode()!= 401){ super.handleError(url,method,response); } } }); HttpEntity<MultiValueMap<String, String>> requestEntity=new HttpEntity<>(body,headers); //3发请求 ResponseEntity<Map> exchange = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class); //4请求中 拿数据 Map body1 = exchange.getBody(); System.out.println(body1); } //请求头中Authorization值的计算方法 public String getHttpHeaders(String clientId, String clientSecret){ //Basic base64(客户端名称:客户端密码) String str = clientId + ":" + clientSecret; byte[] encode = Base64Utils.encode(str.getBytes()); return "Basic "+ new String(encode); } } 4.2.2 业务层 5.4 动态获取用户信息(修改UserDetailService类)
当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是xiaoman都可以访问。因此需要动态获取用户帐号与密码.
5.4.1 用户微服务新增查询方法 @GetMapping("findByAccount/{account}")
public Result<User> findByAccount(@PathVariable("account") String account) {
User user = userService.findByAccount(account);
return Result.success("查询成功", user);
}
5.4.2 user-api中 新增 feign客户端
@FeignClient("user")
public interface UserFeign {
/**
* 根据账号查询用户
* @param account
* @return
*/
@GetMapping("user/findByAccount/{account}")
public Result<User> findByAccount(@PathVariable("account") String account);
}
5.4.3 认证服务导入user-api依赖
<dependency>
<groupId>com.wang</groupId>
<artifactId>hms_service_user_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
5.4.4 认证启动类添加注解
@EnableFeignClients(basePackages = {"com.wang.feign"})
5.4.5 修改UserDetailsServiceImpl 使之动态获取用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private UserFeign userFeign;
/****
* 自定义授权认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用hospital # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/hosp/** # 这个是按照路径匹配,只要以/hosp/开头就符合要求
filters:
# - StripPrefix=1
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 取ipKeyResolver对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
- id: user
uri: lb://user
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}" # 取ipKeyResolver对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
# 在gateway解决跨域问题,不用在controller加@CrossOrigin 注解
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
redis:
host: localhost
port: 9736
6.2 网关全局过滤器
6.2.1 需求
登陆时:放行
用户访问时:
1)判断当前请求是否为登录请求,是的话,则放行
2 ) 判断cookie中是否存在信息, 没有的话,拒绝访问
3)判断redis中令牌是否存在,没有的话,拒绝访问
/**
* @author xiaoman
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
if(path.endsWith("/oauth/login")) {
return chain.filter(exchange);
}
// 判断cookie中有没有jti
HttpCookie cookie = request.getCookies().getFirst("jti");
if(cookie == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String jti = cookie.getValue();
String jwt = stringRedisTemplate.boundValueOps(jti).get();
if(StringUtils.isEmpty(jwt)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 对当前请求增强,让其携带令牌信息(转发)
request.mutate().header("Authorization", "Bearer " + jwt);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
6.2.3 测试
启动gatewayweb
测试登录:localhost:11000/api/oauth/login
测试查询用户:localhost:11000/api/user/userList
查询成功,查看cookie中有jti
本文共计3390个文字,预计阅读时间需要14分钟。
1+简介1.1+基本概念+认证:用户访问系统资源时,系统需验证用户的身份信息,确保身份合法且合规。方式包括常见的账户密码登录、验证码登录、指纹登录等。+授权:用户认证后,系统根据用户权限访问相应资源。
1 简介 1.1 基本概念- 认证:用户访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,常见的账号密码登录,验证码登录,指纹登陆
- 授权:用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限
- 单点登录 SSO:用户在一个系统中登录,其他任意受信任的系统都可以访问,例如在京东主页登陆了,京东其他页面就不需要再登录,这个功能就叫单点登录。
- 第三方账号登录:第三方系统对用户认证通过
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中
Java中有很多用户认证的框架都可以实现单点登录:
- shiro
- CAS
- Spring security CAS
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认 证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的
例如qq音乐登录微信授权的过程:
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
2.1.1 Header头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
2.1.2 Payload第二部分是荷载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
例如
{
"sub": "1234567890",
"name": "张三",
"admin": true
}
2.1.3 Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
secret:签名所使用的密钥。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2.2 生成私钥公钥-运维
JWT令牌生成采用非对称加密算法
2.2.1 生成密钥证书keytool -genkeypair -alias xiaomanhms -keyalg RSA -keypass xiaomanhms -keystore xiaomanhms.jks -storepass xiaomanhms
查询证书信息:
openssl是一个加解密工具包,使用openssl来导出公钥信息。
安装 openssl:slproweb.com/products/Win32OpenSSL.html
cmd进入xiaomanhms.jks文件所在目录执行如下命令:
keytool -list -rfc --keystore xiaomanhms.jks | openssl x509 -inform pem -pubkey
公钥部分为
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行
将hms_user_auth的工程导入到项目中去
然后在父工程pom文件中加入
<module>hms-user-oauth</module>
2.2.3.2 认证服务中创建测试类
用私钥加密
/**
* @author cc
* 基于私钥生成jwt
*/
public class CreateJWTTest {
public static void main(String []args) {
// 1 创建秘钥工厂
ClassPathResource classPathResource = new ClassPathResource("xiaomanhms.jks");
// 2 秘钥库密码
String keyPass = "xiaomanhms";
/**
* 1秘钥位置
* 2秘钥库密码
*/
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, keyPass.toCharArray());
// 2基于工厂拿到私钥
String alias="xiaomanhms";
String password="xiaomanhms";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
//转化为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey)keyPair.getPrivate();
// 3生成jwt
Map<String,String> map = new HashMap<>();
map.put("conpany", "hbue");
map.put("address", "wuhan");
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);
}
}
公钥解密
/**
* @author cc
* 解析jwt令牌测试
*/
public class ParseJwtTest {
public static void main(String[] args) {
//基于公钥去解析jwt
String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoid3VoYW4iLCJjb25wYW55IjoiaGJ1ZSJ9.Zmo4igopc9mKBW4BnbIms4ux4fDq80iV0PvPbwegr9kWT_JP129WCVHn0BkXHTTfMGSmBAwg9XdzvldbWlgTn-IfLPFYfsRFsunwBxMMkqVi4R5IsHczYu-Js2Bg6lZpHwRlbjmyQU0TkzM6bJPnTIxhaxLto7OuLwpSrG27NZUC8BH3BSJPzIp-fzw9NJNrskdt9UWyHcuGmw0dmrFvNKvQ64vQE6_ns0tGHv1vNkkaJdsRbPKVQw-0JpXEbQOVrM3iTdBwbqqXzEkm5fpFEQJzT-SvRkJdRyulvEmS4XL4iO6qzQ3zRLDUoOhV5f3ApUSGfPExtexTcilLTFkiQA";
// 公钥
String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----";
//解析令牌
Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));
//获取负载
String claims = token.getClaims();
System.out.println(claims);
}
}
3 Oauth2.0
3.1 准备工作
导入表oauth_client_details
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条初始化数据,其中加密字符明文为xiaoman
INSERT INTO `oauth_client_details` VALUES ('userweb', NULL, '$2a$10$ejwy6rInOvFWF44gAAgpA.iPWNhmVo7WSG98Yvg4lY9gCmVhxkNRa', 'app', 'authorization_code,password,refresh_token,client_credentials', 'localhost', NULL, 43200, 43200, NULL, NULL);
3.2 Oauth2授权模式介绍
OAuth2.0协议一共支持 4 种不同的授权模式:
- 授权码模式:常见的第三方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用说这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,自己做前后端分离登录就可以采用这种模式。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
一般用的比较多的就是 授权码模式和密码模式
启动hms_user_oauth微服务
浏览器访问
localhost:9200/oauth/authorize?client_id=userweb&response_type=code&scop=app&redirect_uri=localhost
client_id为username, client_secret为password
登陆后会进入进入授权页面:
点击Authorize,相当于我们微信登录qq音乐扫码后点击确认登陆,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=9prf3B就是返回的授权码, 每一个授权码只能使用一次
注意观察:此接口为oauth2提供,项目中没有写controller。
拿到授权码后,申请令牌。
localhost:9200/oauth/token
参数
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 localhost:9200/oauth/check_token?token= [access_token]
刷新令牌是当令牌快过期时重新生成一个令牌,它与授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。
Post:localhost:9200/oauth/token
grant_type: 固定为 refresh_token
refresh_token:刷新令牌
3.2.2 密码模式密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌
3.2.2.1 申请授权码localhost:9200/oauth/token
此链接需要使用 localhost:9101/user/userList
2.携带令牌访问 localhost:9101/user/userList
在localhost:9200/oauth/token"; //不要写死,要部署多个服务 ServiceInstance serviceInstance = loadBalancerClient.choose("user_auth"); URI uri = serviceInstance.getUri(); //localhost:9200 String url=uri+"/oauth/token"; // 2 请求头 // 2.1 请求头 MultiValueMap<String, String> headers=new LinkedMultiValueMap<>(); headers.add("Authorization", getHttpHeaders("userweb","xiaoman")); // 2.2 请求体 MultiValueMap<String, String> body=new LinkedMultiValueMap<>(); body.add("grant_type","password"); body.add("username","userweb"); body.add("password","xiaoman"); // 400 401 不报错,返回回来 restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){ //响应当中有错误 如何处理 @Override public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { if(response.getRawStatusCode() != 400 && response.getRawStatusCode()!= 401){ super.handleError(url,method,response); } } }); HttpEntity<MultiValueMap<String, String>> requestEntity=new HttpEntity<>(body,headers); //3发请求 ResponseEntity<Map> exchange = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class); //4请求中 拿数据 Map body1 = exchange.getBody(); System.out.println(body1); } //请求头中Authorization值的计算方法 public String getHttpHeaders(String clientId, String clientSecret){ //Basic base64(客户端名称:客户端密码) String str = clientId + ":" + clientSecret; byte[] encode = Base64Utils.encode(str.getBytes()); return "Basic "+ new String(encode); } } 4.2.2 业务层 5.4 动态获取用户信息(修改UserDetailService类)
当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是xiaoman都可以访问。因此需要动态获取用户帐号与密码.
5.4.1 用户微服务新增查询方法 @GetMapping("findByAccount/{account}")
public Result<User> findByAccount(@PathVariable("account") String account) {
User user = userService.findByAccount(account);
return Result.success("查询成功", user);
}
5.4.2 user-api中 新增 feign客户端
@FeignClient("user")
public interface UserFeign {
/**
* 根据账号查询用户
* @param account
* @return
*/
@GetMapping("user/findByAccount/{account}")
public Result<User> findByAccount(@PathVariable("account") String account);
}
5.4.3 认证服务导入user-api依赖
<dependency>
<groupId>com.wang</groupId>
<artifactId>hms_service_user_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
5.4.4 认证启动类添加注解
@EnableFeignClients(basePackages = {"com.wang.feign"})
5.4.5 修改UserDetailsServiceImpl 使之动态获取用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private UserFeign userFeign;
/****
* 自定义授权认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用hospital # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/hosp/** # 这个是按照路径匹配,只要以/hosp/开头就符合要求
filters:
# - StripPrefix=1
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 取ipKeyResolver对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
- id: user
uri: lb://user
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}" # 取ipKeyResolver对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
# 在gateway解决跨域问题,不用在controller加@CrossOrigin 注解
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
redis:
host: localhost
port: 9736
6.2 网关全局过滤器
6.2.1 需求
登陆时:放行
用户访问时:
1)判断当前请求是否为登录请求,是的话,则放行
2 ) 判断cookie中是否存在信息, 没有的话,拒绝访问
3)判断redis中令牌是否存在,没有的话,拒绝访问
/**
* @author xiaoman
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
if(path.endsWith("/oauth/login")) {
return chain.filter(exchange);
}
// 判断cookie中有没有jti
HttpCookie cookie = request.getCookies().getFirst("jti");
if(cookie == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String jti = cookie.getValue();
String jwt = stringRedisTemplate.boundValueOps(jti).get();
if(StringUtils.isEmpty(jwt)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 对当前请求增强,让其携带令牌信息(转发)
request.mutate().header("Authorization", "Bearer " + jwt);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
6.2.3 测试
启动gatewayweb
测试登录:localhost:11000/api/oauth/login
测试查询用户:localhost:11000/api/user/userList
查询成功,查看cookie中有jti

