一. 了解Auth0
一.简介
-
Auth0是一个身份管理平台 Auth0主要提供身份认证(即登录账号)与授权服务(不同级别的用户被允许访问的应用资源不同,即权限不同)
-
你的应用可以连接Auth0并定义你想使用哪些身份提供者(IdP),这决定了你的用户可以通过哪些身份提供者(比如:微软AD、谷歌账号、Facebook账号、GitHub账号等)登录你的应用。
-
基于你的应用里所用的编程语言和技术,你可以选择Auth0提供的相关SDK(或者直接嗲用Auth0的API)。这样每次用户尝试身份认证(登录)时,Aurh0会负责他们的身份认证并发送你需要的相关信息(已注册的用户信息、登录成功或失败或未注册、token等)回到你的应用。
-
将你的应用程序与Auth0集成。要登录,用户将被重定向到Auth0的自定义登录页面。用户成功登录后,Auth0会将他们重定向回你的应用,并返回包含其身份验证和用户信息的令牌。(访问令牌可用于调用API和任何外部资源)
-
支持任何技术堆栈
-
SDK与API
API
电脑需要调用手机里面的信息,这时候你会拿一根数据线将电脑手机连接起来,电脑和手机上连接数据线的接口就相当于“API接口”
SDK—软件开发工具包
SDK 就是 Software Development Kit 的缩写,翻译过来——软件开发工具包。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做SDK。
你可以把SDK想象成一个虚拟的程序包,在这个程序包中有一份做好的软件功能,这份程序包几乎是全封闭的,只有一个小小接口可以联通外界,这个接口就是API。 -
帮助定义OpenID Connect 和 JSON Web Tokens等工具的规范。
特征:
通用登录
单点登录
多因素身份验证
违反密码
行动
机器对机器
无密码
二.JSON Web 令牌
JSON
JSON是一种轻量级的数据交换格式。
基于ECMAScript的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。
JSON Web Token概念
JSON Web令牌是一种开放的行业标准RFC 9519方法,是在分布式应用环境间传递身份信息而执行的一种基于JSON的开放标准。
它定义了一种紧凑且独立的方式,用于将信息作为JSON对象在各方之间安全地传输。此信息可以进行验证和信任,因为它是经过数字签名的。
JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
{Hmac算法:HMAC算法是一种基于密钥的报文完整性的验证方法 ,其安全性是建立在Hash加密算法基础上的。它要求通信双方共享密钥、约定算法、对报文进行Hash运算,形成固定长度的认证码。通信双方通过认证码的校验来确定报文的合法性。
HMAC算法可以用来作加密、数字签名、报文验证等 。
一句话总结:HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
HMAC算法是一种执行“校验和”的算法,它通过对数据进行“校验”来检查数据是否被更改了。
}
优点:在各方之间提供保密性。
何时应使用JSON Web令牌?
- 授权:
这是使用JWT的最常见方案。**用户登录后,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。**单点登录时当今广泛使用JWT的一项功能,因为它的开销很小,并且能够跨不同域轻松使用。 - 信息交换:
JSON Web令牌是各方之间安全传输信息的好方法。由于JWT可以签名(例如,使用公钥/私钥对),因此您可以确定发送方就是他们说的人。此外,由于签名是使用表头和有效负载计算的,因此还可以验证内容是否被篡改。
什么是JSON Web令牌结构?
在紧凑的形式中,JSON Web令牌由三个部分组成,由点分隔。
三个部分分别为:页眉 有效载荷 签名
JWT格式通常如下
xxxxx.yyyyy.zzzzz
- 页眉 Header
标头通常由两部分组成:
令牌的类型–type(即JWT)和正在使用的签名算法–alg(如HMAC SHA256 或RSA )
eg:
为默认的页眉
{
"alg": "HS256",
"typ": "JWT"
}
- 有效载荷 ----Claim
令牌的第二部分是有效负载,其中包含声明。
声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明:注册声明、公共声明和私人声明。
注册声明:这些是一组预定义的声明,这些声明不是必需的,但建议提供一组有用的、可互操作的声明。其中一些是:iss(发行人),exp(到期时间),子(主题),aud(受众)等。
公共声明:这些可以由使用JWT的人随意定义。但为避免冲突,应在 IANA JSON Web 令牌注册表中定义它们,或将其定义为包含抗冲突命名空间的 URI。
私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公开声明。
eg:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对有效负载进行 Base64Url 编码,以形成 JSON Web 令牌的第二部分。
- 签名
要创建签名部分,您必须获取编码的标头、编码的有效负载、机密、标头中指定的算法,并对其进行签名。
例如,如果要使用 HMAC SHA256 算法,将按以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中未发生更改,并且,对于使用私钥签名的令牌,它还可以验证 JWT 的发送者是否是它所说的发件人。
结构总内容:
输出是三个Base64-URL字符串(分别为页眉、有效载荷、签名),由点分隔,可以在HTML和HTTP环境中轻松传递,同时基于XML的标准相比更紧凑。
eg:
具有编码的先前标头和有效负载,并使用密钥对其进行签名。
如果要使用JWT并将这些概念付诸实践,可以使用jwt.io调试器来解码、验证和生成JWT。
eg:
JSON Web令牌如何工作
在身份验证中,当用户用其凭据成功登录时,将返回JSON Web令牌。
由于令牌是凭据,因此必须非常小心地防止安全问题。通常,保留令牌的时间不应超过所需的时间。
每当用户想要访问受保护的路由或资源时,用户代理都应发送JWT,通常在授权标头中使用持有者架构。标头的内容应如下所示:
Authorization: Bearer <token>
【
-
浅聊微服务
微服务其实就是服务化思路的一种最佳实践方向,遵循SOA(面向服务的架构)的思路。
早些年的服务实现和实施思路是将很多功能从开发到交付都打包成一个很大的服务单元,而微服务实现和实施思路则更强调功能趋向单一,服务单元小型化和微型化。
从思路和理念来说微服务就是要倡导大家尽量将功能进行拆分,将服务粒度做小,使其可以独立承担对外服务的职责。
应用
对于微服务来说,如果团队不大,软件复杂度不高,那么,使用微服务的形式进行服务化治理是比较合适的,而且,这种方式怼运维和各种基础设施的要求也不高。 -
权限认证中的有状态和无状态
我们在设计构建一个系统的时候,权限管理和用户认证是最基本的功能,其中关于用户认证这块是一个比较常见的模块。在已有的方案中,我们最常见的就是保存到tomcat中的session对象中。
随着微服务的兴起,一种新的认证方法又火了起来,那就是JWT,分别说明权限认证方式的有状态无状态。
有状态和无状态最大的区别就是服务端会不会保存客户端的信息。
有状态认证
有状态的认证,以cookie-session模型为例,当客户端第一次请求服务端的时候,服务端会返回客户端一个唯一表示(默认在cookie中),并保存对应的客户端信息。客户端接受到唯一标识之后,将标识位保存到本地cookie中,以后的每次请求都携带此cookie,服务端根据此cookie表示可以判断用户是谁,然后查到对应用户的信息。
请求认证过程(以tomcat为例):
1、客户端向服务端发起请求;
2、第一次客户端发起请求,服务端创建一个 key 为JSESSIONID的值,并写入到客户端的cookie中,同时在服务端的Session Manager中创建一个对象,保存这个 JSESSIONID 对应的信息;
3、以后客户端每次请求,都会根据cookie进行区别,我们可以通过 session.setAttrbutie,session.getAttrbuite 等方法,拓展用户信息,根据用户信息做一些业务判断等;
4、Session Manger 中维护有一个定时器,当 JSESSIONID 对应的信息长时间没有访问(默认30分钟),或者显性调用 session.invalidate 方法,那么这个对应的信息将会被删除。
无状态认证
无状态的认证,客户端在提交身份信息,服务端验证身份后,根据一定的算法生成一个token令牌返回给客户端,之后每次请求服务端,用户都要携带此令牌,服务端接收到令牌之后进行校验,校验通过后,提取令牌中的信息来区别用户
请求认证过程:
1、执行登录操作,用户端发送账号密码等信息;
2、服务端校验账号密码是否正确,如果正确,根据对应的用户信息和服务端秘钥生成 JWT 令牌,然后通过response.setHeader 返回给客户端(此处假设生成了一个名为 x-auth-token 的令牌);
3、客户端在返回成功之后,将Header中的x-auth-token 保存到本地的LocalStorage中;
4、客户端在以后每次请求服务端时候,都在header中携带x-auth-token令牌的值;
5、服务端每接受到请求之后,判断hader中是否包含x-auth-token,token 是否有效,然后通过 BASE 64 算法 decode,根据解密后的参数,判断当前 token 是否在有效期,所访问的接口是否有权限等操作。
优势
1、因为服务端不保留客户端的任何信息,每次只需要通过特定的算法进行校验,节省了大量存储空间;
2、方便水平扩容,不需要 SSO Server,只要保证新的应用采用同样的验证算法,就可以验证通过并获得对应信息。
劣势
当客户端的token被盗用,或者需要手动封禁某个用户的时候,没办法对此token进行操作,必须等待token失效(如果在服务端维护token和用户的关系,技术可以实现,但是违背无状态的设计理念)。
应用实战
1、生成的 token 中携带用户常用信息,但是不携带用户的敏感信息,比如密码手机号等等,因为这些信息通过BASE 64 可以解密出来的;
2、要处理服务端主动禁用某个 token ,可以采用黑名单措施,每次请求前判断当前token是否已经被禁用;
3、token 中的信息除了基本信息外,还应该携带比如签发时间、有效时间、刷新token等字段,用来处理token的续约问题。
{
SSO system :单点登录系统
是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
}
】
在某些情况下,这可能是一种无状态的授权机制。服务器的受保护路由将在标头中检查有效的JWT,如果存在,将允许用户访问受保护的资源。
如果JWT包含必要的数据,则可能会减少查询数据库以进行某些操作的需要。
如果令牌在标头中发送,则跨域资源共享(CORS)不会成为问题,因为它不使用cookie。
如何获取JWT并用于访问API或资源
1.应用程序或客户端请求对授权服务器进行授权。这是通过不同的授权流之一执行的。例如,典型的 OpenID Connect 兼容 Web 应用程序将使用授权代码流通过终结点。/oauth/authorize
2.授予授权后,授权服务器将向应用程序返回访问令牌。
3.应用程序使用访问令牌访问受保护的资源(如 API)。
注意:对于已签名的令牌,令牌中包含的所有信息都会向用户或者其他地方公开,这意味着不应将机密信息放在令牌中。
三.Spring Boot与JWT
java与JWT
https://github.com/auth0/java-jwt
1.引入JWT的依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.1</version>
</dependency>
2.生成JWT(获取令牌)
jwt bulder
页眉----HashMap<String , Object>
withHeader()
声明----name value
withClaim()
签名-----Algorithm.(算法) 算法中间的字符串为秘钥
sign()
指定令牌的过期时间-----Calender
withExpiresAt()
HashMap<String,Object> map = new HashMap<>();
map.put("alg","HS256");
map.put("typ", "JWT");
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,20);
String token = JWT.create().withHeader(map)//header
.withClaim("userId",21)//payload
.withClaim("userName","huangjuan")//payload
.withExpiresAt(instance.getTime())//指定令牌过期时间
.sign(Algorithm.HMAC256("2r34w#"));//签名
输出内容
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6Imh1YW5nanVhbiIsImV4cCI6MTY1MDQ0MjEzMCwidXNlcklkIjoyMX0.XO9HAI6IEqb-P6BkqVedbVINOsWFqNNJm4SPQtKwoMQ
3.验证令牌
(1)验证签名
注意验签的算法要一致
JWT.require(Algorithm.HMAC256(" "));
(2)构建一个验签对象JWTVerifier
build
(3)验证对象的验证方法,进行验证
verify()
注意:token验证通过后,验证的信息都放在DecodedJWT对象中间
(4)验证通过,则可以取令牌的内容 ----get…()
报错:
过期
//创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("2r34w#")).build();
//验证
DecodedJWT verify = jwtVerifier.verify("");
//获取数据
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaim("userName").asString());
System.out.println("过期时间:"+verify.getExpiresAt());
结果:
21
huangjuan
过期时间:Wed Apr 20 16:34:51 GMT+08:00 2022
封装工具类
1.对于传入的信息,创建令牌
//签名设置固定签名
private static final String SIGN= "23#huwqq!";
/*
* 生成token header.payload.sing
*
* 传入的信息,通过循环传入token builder
*/
public static String getToken(Map<String,String> map){
//设置生效时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//默认7天过期
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime())//指定令牌过期时间
.sign(Algorithm.HMAC256(SIGN)); //sign
return token;
}
注意:
withClaim()没有String和Object的重载方法。
2.验证令牌,根据token信息,获取用户信息
/*
* 验证token 合法性
*
* public static void verify(String token){
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
*
*获取token信息方法
* public static DecodedJWT get(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
* */
/*
* 验证token 合法性
*
* 并返回token信息
* */
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
有效负载声明
1…getIssuer();
返回“颁发者”值或 null(如果未在有效负载中定义)。
String issuer = jwt.getIssuer();
返回值调用:
asBoolean():返回布尔值,如果无法转换,则返回 null。
asInt():返回 Integer 值,如果无法转换则返回 null。
asDouble():返回 Double 值,如果无法转换则返回 null 值。
asLong():返回 Long 值,如果无法转换则返回 null 值。
asString():返回 String 值,如果无法转换,则返回 null。
asDate():返回 Date 值,如果无法转换,则返回 null。这必须是 NumericDate(Unix Epoch/Timestamp)。请注意,JWT 标准指定所有 NumericDate 值必须以秒为单位。
as(class):返回解析为类类型的值。对于集合,应使用 and 方法。asArrayasList
asMap():返回解析为 Map<String,Object>的值。
asArray(class):返回解析为类类型元素数组的值,如果值不是 JSON 数组,则返回 null。
asList(class):返回解析为类类型元素列表的值,如果值不是 JSON 数组,则返回 null。
结合SpringBoot
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--JWT依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.6.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>3.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.1</version>
</dependency>
<!--mybatis包-->
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!--junit包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
</dependency>
<!--
log4j包
-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--lombok包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<!--ehcache包-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
<!--引入druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<!--postgresql-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!--Spring Data JPA的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--junit4测试单元的工具-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!--hibernate 对jpa的支持包-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.32.Final</version>
</dependency>
2.引入配置
注意:我们这里使用的为postgresql不是mysql
spring:
datasource:
url: jdbc:postgresql://192.168.43.204:5432/exam?characterEncoding=UTF-8
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
jpa:
database: Postgresql
show-sql: true
open-in-view: false
hibernate:
dialect: org.hibernate.dialect.PostgreSQL9Dialect
ddl-auto: update
temp:
use_jdbc_metadata_defaults: false
#端口号
server:
port: 80
创建数据库
在这里插入代码片
报错:
java.sql.SQLFeatureNotSupportedException: 这个 org.postgresql.jdbc.PgConnection.createClob() 方法尚未被实作。
application.yml增加以下属性
spring:
jpa:
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
今天碰到了一个很无语的问题,整合SpringDataJPA和Postgresql的时候,无论调repository的什么方法,都报空指针异常。
解决:
我居然不小心把springboot的测试注解删了!!!
应用
1.从数据库完成认证,并生成Token
//日志输出信息
log.info("用户名:[{}]",userInfo.userName);
log.info("密码:[{}]",userInfo.password);
//返回登录是否成功
try {
UserInfo userDB = userInfoService.login(userInfo);
/*JWT*/
//payload里面放信息
Map<String,String> payload = new HashMap<>();
payload.put("id", String.valueOf(userDB.getId()));
payload.put("name",userDB.getUserName());
//生成JWT令牌
String token = JWTUtils.getToken(payload);
return JsonUtils.success().add("token",token);
}catch (Exception e){
return JsonUtils.fail();
}
2.token的验证可以放在拦截器中
分布式的:放在网关中
package com.huangjuan.demo.interceptors;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.huangjuan.demo.Utils.JWTUtils;
import com.huangjuan.demo.Utils.JsonUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JWTInterceptor implements HandlerInterceptor {
//注意:官方建议将token放在请求头
//预先处理
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的令牌
String token = request.getHeader("token");
try{
DecodedJWT verify = JWTUtils.verify(token);//验证令牌
return true; //放行请求
}catch (SignatureVerificationException e){
e.printStackTrace();
JsonUtils.fail();
}catch (TokenExpiredException e){
e.printStackTrace();
JsonUtils.fail();
}catch (AlgorithmMismatchException e){
e.printStackTrace();
JsonUtils.fail();
}catch (Exception e){
e.printStackTrace();
JsonUtils.fail();
}
return false;
}
}
3.配置拦截器
package com.huangjuan.demo.Config;
import com.huangjuan.demo.interceptors.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//拦截器配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
//新增拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") //拦截
.excludePathPatterns("/user/**");//放行
}
}
token在request的header中
request.getHeader(“token”);