目录
基础概念
依赖
生成令牌
工具类
控制层
解析令牌
工具类
网关过滤器
效果
基础概念
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案
为什么使用:
传统的intenet服务的认证是通过session进行的,当用户通过了安全认证后,则在服务端的session对象中保存该用户的认证信息,这样该用户对服务的访问被认为是安全的。这种模式的最大问题是没有分布式架构,不方便进行横向扩展,这种模式只适合于单体应用模式。如果需要进行服务集群则需要处理好共享session的问题。 如果一个庞大的系统需要按服务分解为多个独立的服务,使用分布式架构,则这种方式更难处理。使用jwt可以方便的处理上面提到的问题。
验证过程:
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中,此后在于服务器的交互中都携带者JWT信息,它验证的方法其实很简单,只要把header做base64url解码,就能知道JWT用的什么算法做的签名,然后用这个算法,再次用同样的逻辑对header和payload做一次签名,并比较这个签名是否与JWT本身包含的第三个部分的串是否完全相同,只要不同,就可以认为这个JWT是一个被篡改过的串。
依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--javax.xml.bind.DatatypeConverter java8 以后要加-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
生成令牌
在登录过程中新建令牌,所以需要写在生产者中,并且根据数据库的用户和密码登录并判断其权限,根据给定的母版key、用户名、密码、权限生成其令牌。注意,这里设定了1小时候令牌过期。
工具类
@Component
public class JwtUtil {
//加密 解密时的密钥 用来生成key
public static final String JWT_KEY = "IT1995";
/**
* \* 生成加密后的秘钥 secretKey
* <p>
* \* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length,
"AES");
return key;
}
/**
* \* 登录成功后生成token* \* @param id
* \* @param account
* \* @param role
* \* @param ttlMillis
* \* @return
*/
public static String createJWT(String id, Account account, String role, long
ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
long nowMillis = System.currentTimeMillis();//生成JWT的时间
Date now = new Date(nowMillis);
SecretKey key = generalKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
Map claims = new HashMap();
claims.put("name", account.getUsername());
claims.put("role", role);
JwtBuilder builder = Jwts.builder()//这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims) //如果有私有声明,一定要先设置这个自己创 建的私有的声明,这 个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声 明的 创建用户表
.setId(id) //设置jti(JWT ID):是JWT的唯一标识,根 据业务需要,这个可以设置为 一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) //iat: jwt的签发时间
// .setSubject(subject) //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, key);//设置签名使用的签名算法和签名使用的秘钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp); //设置过期时间
}
return builder.compact(); //就开始压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
}
/**
* \* 密码解密对比方法4.编写mapper
* 5.编写service根据用户名取的用户信息和权限
* \* @param password
* \* @param repassword
* \* @return
*/
public static boolean matchsPassword(String password, String repassword) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.matches(password, repassword);
}
}
控制层
@RestController
public class LoginController {
@Autowired
AccountService accountService;
@RequestMapping("/login")
public String Login(Account account) {
String token = "没有token";
//通过用户名拿到用户实体类
Account result = accountService.loginByUserName(account.getUsername());
if (result != null) {
//然后进行密码的加密后的比较
boolean b = JwtUtil.matchsPassword(account.getPassword(), result.getPassword());
//如果密码一致表示登录成功
if (result != null && b) {
List<Authority> author = accountService.findAuthoritiesByUsername(account.getUsername());
token = JwtUtil.createJWT(UUID.randomUUID().toString(), result, author.get(0).getAuthority(), 3600L * 1000);
}
}
return token;
}
}
解析令牌
给定令牌并解析,根据不同token,解析出对应的用户名、密码和权限,并判断访问内容,根据访问路径中的敏感词判断是否有权限查看页面。
工具类
@Component
public class JwtUtil {
//加密 解密时的密钥 用来生成key
public static final String JWT_KEY = "IT1995";
/**
* \* 生成加密后的秘钥 secretKey
* <p>
* \* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length,
"AES");
return key;
}
/**
* \* 解析token
* \* @param jwt
* \* @return
*/
public static Claims parseJWT(String jwt) {
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
Claims claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(jwt).getBody();//设置需要解析的jwt
return claims;
}
}
网关过滤器
@Component
public class gatewayConfig implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getPath().value();
// 1. 获取请求
ServerHttpRequest request = exchange.getRequest();
//2. 则获取响应
ServerHttpResponse response = exchange.getResponse();
//3. 如果是登录请求则放行
if (request.getURI().getPath().contains("/login") || request.getURI().getPath().contains("/logout")) {
return chain.filter(exchange);
}
//4. 获取请求头
HttpHeaders headers = request.getHeaders();
//5. 请求头中获取令牌
String token = headers.getFirst(AUTHORIZE_TOKEN);
//6. 判断请求头中是否有令牌
if (StringUtils.isEmpty(token)) {
//7. 响应中放入返回的状态吗, 没有权限访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//8. 返回
return response.setComplete();
}
//9. 如果请求头中有令牌则解析令牌
try {
Claims claims = JwtUtil.parseJWT(token);
//获取token中的权限然后根据权限做访问权限
String role = (String) claims.get("role");
System.out.println(role);
if (role.equals("vip")) {
return chain.filter(exchange);
}
if (role.equals("common")) {
if (request.getURI().getPath().contains("/vip")) {
response.setStatusCode(HttpStatus.FORBIDDEN);
//11. 返回
return response.setComplete();
}
}
} catch (Exception e) {
e.printStackTrace();
//10. 解析jwt令牌出错, 说明令牌过期或者伪造等不合法情况出现
response.setStatusCode(HttpStatus.FORBIDDEN);
//11. 返回
return response.setComplete();
}
//12. 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
效果
只有vip用户才能查看vip页面,普通用户查看vip页面403权限不足错误。如果没有登录令牌查看页面,401错误。
没有登录令牌
vip登录成功
普通用户访问vip 403